[{"data":1,"prerenderedAt":2638},["ShallowReactive",2],{"page-\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fcustomizing-fastapi-openapi-schema\u002F":3,"faq-schema-\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fcustomizing-fastapi-openapi-schema\u002F":2617},{"id":4,"title":5,"body":6,"description":2607,"extension":2608,"meta":2609,"navigation":348,"path":2613,"seo":2614,"stem":2615,"__hash__":2616},"content\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fcustomizing-fastapi-openapi-schema\u002Findex.md","Customizing the FastAPI OpenAPI Schema",{"type":7,"value":8,"toc":2595},"minimark",[9,13,37,52,57,60,150,293,297,307,595,602,741,824,828,854,860,1479,1486,1490,1499,1954,1968,2052,2056,2083,2086,2359,2379,2457,2461,2464,2471,2475,2492,2499,2503,2509,2512,2516,2523,2529,2538,2544,2550,2554,2591],[10,11,5],"h1",{"id":12},"customizing-the-fastapi-openapi-schema",[14,15,16,17,21,22,25,26,31,32,36],"p",{},"There are exactly two places you can change what FastAPI publishes at ",[18,19,20],"code",{},"\u002Fopenapi.json",", and picking the wrong one costs you a week of debugging. You either declare the customization on the route and the model, where the framework builds it into the document for you, or you rewrite the finished dictionary inside a custom ",[18,23,24],{},"openapi()"," callable. This page resolves which change belongs in which layer, and it is part of the ",[27,28,30],"a",{"href":29},"\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002F","Documenting APIs with OpenAPI"," guide inside ",[27,33,35],{"href":34},"\u002Fgetting-started-with-python-apis-for-builders\u002F","Getting Started with Python APIs for Builders",".",[14,38,39,40,43,44,47,48,51],{},"The commercial stake is concrete. A customer who generates a client from your spec either gets ",[18,41,42],{},"client.create_forecast()"," and a working ",[18,45,46],{},"X-API-Key"," header, or gets ",[18,49,50],{},"client.create_forecast_v1_forecasts__post()"," and a 401 they email you about. The difference is roughly ninety lines of configuration, and it decides whether your integration support load grows with revenue or stays flat.",[53,54,56],"h2",{"id":55},"the-two-layers-and-what-belongs-in-each","The Two Layers, and What Belongs in Each",[14,58,59],{},"Default to the declarative layer. Anything FastAPI models natively — operation IDs, response codes, examples, tags, exclusion — should live on the decorator or the Pydantic model, because that keeps the document and the runtime behaviour impossible to desynchronise. Reach for a post-processor only when OpenAPI has a field FastAPI does not expose: vendor extensions, component renaming, document-level security defaults, or scrubbing paths out of a published artifact.",[61,62,63,79],"table",{},[64,65,66],"thead",{},[67,68,69,73,76],"tr",{},[70,71,72],"th",{},"Goal",[70,74,75],{},"Declarative",[70,77,78],{},"Post-process",[80,81,82,96,108,121,131,140],"tbody",{},[67,83,84,88,93],{},[85,86,87],"td",{},"Method name in the SDK",[85,89,90],{},[18,91,92],{},"operation_id",[85,94,95],{},"no",[67,97,98,101,106],{},[85,99,100],{},"Named request examples",[85,102,103],{},[18,104,105],{},"openapi_examples",[85,107,95],{},[67,109,110,113,118],{},[85,111,112],{},"Tag order and blurbs",[85,114,115],{},[18,116,117],{},"openapi_tags",[85,119,120],{},"rarely",[67,122,123,126,128],{},[85,124,125],{},"Vendor extensions",[85,127,95],{},[85,129,130],{},"yes",[67,132,133,136,138],{},[85,134,135],{},"Rename schema components",[85,137,95],{},[85,139,130],{},[67,141,142,145,148],{},[85,143,144],{},"Strip internal paths",[85,146,147],{},"partly",[85,149,130],{},[151,152,160,161,160,165,160,169,160,176,160,195,160,203,160,212,160,218,160,222,160,226,160,229,160,233,160,237,160,244,160,252,160,257,160,261,160,265,160,270,160,274,160,277,160,280,160,283,160,286,160,289],"svg",{"viewBox":153,"role":154,"ariaLabelledBy":155,"xmlns":158,"style":159},"0 0 760 280","img",[156,157],"cfos-layers-t","cfos-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[162,163,164],"title",{"id":156},"Declarative layer and post-processing layer around the schema builder",[166,167,168],"desc",{"id":157},"Route decorators and models feed the FastAPI schema builder, whose output a post-processor then edits for vendor extensions, renaming, and security defaults before the artifact ships.",[170,171],"rect",{"x":172,"y":172,"width":173,"height":174,"fill":175},"0","760","280","var(--c-surface)",[177,178,179,180,160],"defs",{},"\n    ",[181,182,189,190,179],"marker",{"id":183,"viewBox":184,"refX":185,"refY":186,"markerWidth":187,"markerHeight":187,"orient":188},"cfos-layers-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[191,192],"path",{"d":193,"fill":194},"M0 0 L10 5 L0 10 z","var(--c-text-muted)",[196,197,202],"text",{"x":198,"y":199,"fill":200,"style":201},"380","30","var(--c-text)","text-anchor:middle;font-size:14;font-family:var(--font-sans);","Where each customization belongs",[170,204],{"x":205,"y":206,"width":207,"height":208,"rx":209,"fill":175,"stroke":210,"style":211},"24","52","200","172","10","var(--c-blue)","stroke-width:2;",[196,213,217],{"x":214,"y":215,"fill":200,"style":216},"124","80","text-anchor:middle;font-size:12;font-family:var(--font-sans);","Declarative layer",[196,219,92],{"x":214,"y":220,"fill":194,"style":221},"106","text-anchor:middle;font-size:11;font-family:var(--font-sans);",[196,223,225],{"x":214,"y":224,"fill":194,"style":221},"130","response models",[196,227,105],{"x":214,"y":228,"fill":194,"style":221},"154",[196,230,232],{"x":214,"y":231,"fill":194,"style":221},"178","tags and summaries",[196,234,236],{"x":214,"y":235,"fill":194,"style":221},"202","include_in_schema",[238,239],"line",{"x1":240,"y1":241,"x2":242,"y2":241,"stroke":194,"style":243},"226","142","260","stroke-width:2;marker-end:url(#cfos-layers-arrow);",[170,245],{"x":246,"y":247,"width":248,"height":249,"rx":209,"fill":250,"stroke":251,"style":211},"264","100","170","84","var(--c-surface-alt)","var(--c-teal)",[196,253,256],{"x":254,"y":255,"fill":200,"style":216},"349","132","Schema builder",[196,258,260],{"x":254,"y":259,"fill":194,"style":221},"156","get_openapi(routes=...)",[238,262],{"x1":263,"y1":241,"x2":264,"y2":241,"stroke":194,"style":243},"436","470",[170,266],{"x":267,"y":206,"width":268,"height":208,"rx":209,"fill":175,"stroke":269,"style":211},"474","212","var(--c-coral)",[196,271,273],{"x":272,"y":215,"fill":200,"style":216},"580","Post-processing layer",[196,275,276],{"x":272,"y":220,"fill":194,"style":221},"vendor extensions",[196,278,279],{"x":272,"y":224,"fill":194,"style":221},"component renaming",[196,281,282],{"x":272,"y":228,"fill":194,"style":221},"document security",[196,284,285],{"x":272,"y":231,"fill":194,"style":221},"path scrubbing",[196,287,288],{"x":272,"y":235,"fill":194,"style":221},"server variables",[196,290,292],{"x":198,"y":291,"fill":200,"style":216},"254","Both layers produce one cached artifact per worker process",[53,294,296],{"id":295},"operation-ids-that-read-like-an-sdk","Operation IDs That Read Like an SDK",[14,298,299,300,302,303,306],{},"FastAPI derives an operation ID from the function name, path, and verb unless you intervene. Setting ",[18,301,92],{}," on every decorator works but rots: someone adds a route in a hurry, forgets the argument, and one ugly method appears in the next generated client. Install a ",[18,304,305],{},"generate_unique_id_function"," on the application instead, then override per route only when the derived name is wrong.",[308,309,314],"pre",{"className":310,"code":311,"language":312,"meta":313,"style":313},"language-python shiki shiki-themes github-light github-dark","# app\u002Fnaming.py\nimport os\nimport re\n\nfrom fastapi.routing import APIRoute\n\n_WORDS = re.compile(r\"[^a-z0-9]+\")\n\n\ndef camel_operation_id(route: APIRoute) -> str:\n    \"\"\"Turn `create_forecast` into `createForecast`, prefixing by tag if asked.\"\"\"\n    words = [w for w in _WORDS.split(route.name.lower()) if w]\n    if os.getenv(\"OPENAPI_ID_PREFIX_TAGS\", \"false\").lower() == \"true\" and route.tags:\n        tag = _WORDS.split(str(route.tags[0]).lower())[0]\n        words = [tag, *words]\n    head, *tail = words\n    return head + \"\".join(part.capitalize() for part in tail)\n","python","",[18,315,316,324,335,343,350,364,369,406,411,416,435,441,474,507,535,552,568],{"__ignoreMap":313},[317,318,320],"span",{"class":238,"line":319},1,[317,321,323],{"class":322},"sJ8bj","# app\u002Fnaming.py\n",[317,325,327,331],{"class":238,"line":326},2,[317,328,330],{"class":329},"szBVR","import",[317,332,334],{"class":333},"sVt8B"," os\n",[317,336,338,340],{"class":238,"line":337},3,[317,339,330],{"class":329},[317,341,342],{"class":333}," re\n",[317,344,346],{"class":238,"line":345},4,[317,347,349],{"emptyLinePlaceholder":348},true,"\n",[317,351,353,356,359,361],{"class":238,"line":352},5,[317,354,355],{"class":329},"from",[317,357,358],{"class":333}," fastapi.routing ",[317,360,330],{"class":329},[317,362,363],{"class":333}," APIRoute\n",[317,365,367],{"class":238,"line":366},6,[317,368,349],{"emptyLinePlaceholder":348},[317,370,372,376,379,382,385,389,392,395,398,401,403],{"class":238,"line":371},7,[317,373,375],{"class":374},"sj4cs","_WORDS",[317,377,378],{"class":329}," =",[317,380,381],{"class":333}," re.compile(",[317,383,384],{"class":329},"r",[317,386,388],{"class":387},"sZZnC","\"",[317,390,391],{"class":374},"[",[317,393,394],{"class":329},"^",[317,396,397],{"class":374},"a-z0-9]",[317,399,400],{"class":329},"+",[317,402,388],{"class":387},[317,404,405],{"class":333},")\n",[317,407,409],{"class":238,"line":408},8,[317,410,349],{"emptyLinePlaceholder":348},[317,412,414],{"class":238,"line":413},9,[317,415,349],{"emptyLinePlaceholder":348},[317,417,419,422,426,429,432],{"class":238,"line":418},10,[317,420,421],{"class":329},"def",[317,423,425],{"class":424},"sScJk"," camel_operation_id",[317,427,428],{"class":333},"(route: APIRoute) -> ",[317,430,431],{"class":374},"str",[317,433,434],{"class":333},":\n",[317,436,438],{"class":238,"line":437},11,[317,439,440],{"class":387},"    \"\"\"Turn `create_forecast` into `createForecast`, prefixing by tag if asked.\"\"\"\n",[317,442,444,447,450,453,456,459,462,465,468,471],{"class":238,"line":443},12,[317,445,446],{"class":333},"    words ",[317,448,449],{"class":329},"=",[317,451,452],{"class":333}," [w ",[317,454,455],{"class":329},"for",[317,457,458],{"class":333}," w ",[317,460,461],{"class":329},"in",[317,463,464],{"class":374}," _WORDS",[317,466,467],{"class":333},".split(route.name.lower()) ",[317,469,470],{"class":329},"if",[317,472,473],{"class":333}," w]\n",[317,475,477,480,483,486,489,492,495,498,501,504],{"class":238,"line":476},13,[317,478,479],{"class":329},"    if",[317,481,482],{"class":333}," os.getenv(",[317,484,485],{"class":387},"\"OPENAPI_ID_PREFIX_TAGS\"",[317,487,488],{"class":333},", ",[317,490,491],{"class":387},"\"false\"",[317,493,494],{"class":333},").lower() ",[317,496,497],{"class":329},"==",[317,499,500],{"class":387}," \"true\"",[317,502,503],{"class":329}," and",[317,505,506],{"class":333}," route.tags:\n",[317,508,510,513,515,517,520,522,525,527,530,532],{"class":238,"line":509},14,[317,511,512],{"class":333},"        tag ",[317,514,449],{"class":329},[317,516,464],{"class":374},[317,518,519],{"class":333},".split(",[317,521,431],{"class":374},[317,523,524],{"class":333},"(route.tags[",[317,526,172],{"class":374},[317,528,529],{"class":333},"]).lower())[",[317,531,172],{"class":374},[317,533,534],{"class":333},"]\n",[317,536,538,541,543,546,549],{"class":238,"line":537},15,[317,539,540],{"class":333},"        words ",[317,542,449],{"class":329},[317,544,545],{"class":333}," [tag, ",[317,547,548],{"class":329},"*",[317,550,551],{"class":333},"words]\n",[317,553,555,558,560,563,565],{"class":238,"line":554},16,[317,556,557],{"class":333},"    head, ",[317,559,548],{"class":329},[317,561,562],{"class":333},"tail ",[317,564,449],{"class":329},[317,566,567],{"class":333}," words\n",[317,569,571,574,577,579,582,585,587,590,592],{"class":238,"line":570},17,[317,572,573],{"class":329},"    return",[317,575,576],{"class":333}," head ",[317,578,400],{"class":329},[317,580,581],{"class":387}," \"\"",[317,583,584],{"class":333},".join(part.capitalize() ",[317,586,455],{"class":329},[317,588,589],{"class":333}," part ",[317,591,461],{"class":329},[317,593,594],{"class":333}," tail)\n",[14,596,597,598,601],{},"Wire it into the factory once and every router inherits it. Prefixing by tag matters when two areas of your API both expose a ",[18,599,600],{},"list"," endpoint — most generators namespace by tag anyway, but a couple flatten everything into a single class and collide.",[308,603,605],{"className":310,"code":604,"language":312,"meta":313,"style":313},"# app\u002Fmain.py\nimport os\n\nfrom fastapi import FastAPI\n\nfrom app.naming import camel_operation_id\n\n\ndef create_app() -> FastAPI:\n    app = FastAPI(\n        title=os.getenv(\"API_TITLE\", \"Untitled API\"),\n        version=os.getenv(\"API_VERSION\", \"0.0.0\"),\n        generate_unique_id_function=camel_operation_id,\n    )\n    return app\n",[18,606,607,612,618,622,634,638,650,654,658,668,678,700,719,729,734],{"__ignoreMap":313},[317,608,609],{"class":238,"line":319},[317,610,611],{"class":322},"# app\u002Fmain.py\n",[317,613,614,616],{"class":238,"line":326},[317,615,330],{"class":329},[317,617,334],{"class":333},[317,619,620],{"class":238,"line":337},[317,621,349],{"emptyLinePlaceholder":348},[317,623,624,626,629,631],{"class":238,"line":345},[317,625,355],{"class":329},[317,627,628],{"class":333}," fastapi ",[317,630,330],{"class":329},[317,632,633],{"class":333}," FastAPI\n",[317,635,636],{"class":238,"line":352},[317,637,349],{"emptyLinePlaceholder":348},[317,639,640,642,645,647],{"class":238,"line":366},[317,641,355],{"class":329},[317,643,644],{"class":333}," app.naming ",[317,646,330],{"class":329},[317,648,649],{"class":333}," camel_operation_id\n",[317,651,652],{"class":238,"line":371},[317,653,349],{"emptyLinePlaceholder":348},[317,655,656],{"class":238,"line":408},[317,657,349],{"emptyLinePlaceholder":348},[317,659,660,662,665],{"class":238,"line":413},[317,661,421],{"class":329},[317,663,664],{"class":424}," create_app",[317,666,667],{"class":333},"() -> FastAPI:\n",[317,669,670,673,675],{"class":238,"line":418},[317,671,672],{"class":333},"    app ",[317,674,449],{"class":329},[317,676,677],{"class":333}," FastAPI(\n",[317,679,680,684,686,689,692,694,697],{"class":238,"line":437},[317,681,683],{"class":682},"s4XuR","        title",[317,685,449],{"class":329},[317,687,688],{"class":333},"os.getenv(",[317,690,691],{"class":387},"\"API_TITLE\"",[317,693,488],{"class":333},[317,695,696],{"class":387},"\"Untitled API\"",[317,698,699],{"class":333},"),\n",[317,701,702,705,707,709,712,714,717],{"class":238,"line":443},[317,703,704],{"class":682},"        version",[317,706,449],{"class":329},[317,708,688],{"class":333},[317,710,711],{"class":387},"\"API_VERSION\"",[317,713,488],{"class":333},[317,715,716],{"class":387},"\"0.0.0\"",[317,718,699],{"class":333},[317,720,721,724,726],{"class":238,"line":476},[317,722,723],{"class":682},"        generate_unique_id_function",[317,725,449],{"class":329},[317,727,728],{"class":333},"camel_operation_id,\n",[317,730,731],{"class":238,"line":509},[317,732,733],{"class":333},"    )\n",[317,735,736,738],{"class":238,"line":537},[317,737,573],{"class":329},[317,739,740],{"class":333}," app\n",[151,742,160,747,160,750,160,753,160,756,160,763,160,768,160,773,160,778,160,782,160,788,160,794,160,798,160,801,160,804,160,807,160,810,160,813,160,815,160,817,160,819],{"viewBox":743,"role":154,"ariaLabelledBy":744,"xmlns":158,"style":159},"0 0 760 270",[745,746],"cfos-ids-t","cfos-ids-d",[162,748,749],{"id":745},"Default operation IDs versus derived camelCase IDs in a generated client",[166,751,752],{"id":746},"The default identifier concatenates function name, path, and verb producing an unusable SDK method, while the derived identifier yields a short readable call.",[170,754],{"x":172,"y":172,"width":173,"height":755,"fill":175},"270",[177,757,179,758,160],{},[181,759,189,761,179],{"id":760,"viewBox":184,"refX":185,"refY":186,"markerWidth":187,"markerHeight":187,"orient":188},"cfos-ids-arrow",[191,762],{"d":193,"fill":194},[196,764,767],{"x":199,"y":765,"fill":200,"style":766},"36","text-anchor:start;font-size:14;font-family:var(--font-sans);","What the customer types after generating a client",[170,769],{"x":199,"y":770,"width":771,"height":772,"rx":209,"fill":175,"stroke":269,"style":211},"60","300","70",[196,774,777],{"x":775,"y":776,"fill":194,"style":221},"180","86","default operationId",[196,779,781],{"x":775,"y":780,"fill":200,"style":216},"110","create_forecast_v1_forecasts_post",[238,783],{"x1":784,"y1":785,"x2":786,"y2":785,"stroke":194,"style":787},"332","95","368","stroke-width:2;marker-end:url(#cfos-ids-arrow);",[170,789],{"x":790,"y":770,"width":791,"height":772,"rx":209,"fill":250,"stroke":792,"style":793},"372","358","var(--c-border)","stroke-width:1.5;",[196,795,797],{"x":796,"y":776,"fill":194,"style":221},"551","generated call",[196,799,800],{"x":796,"y":780,"fill":200,"style":216},"client.create_forecast_v1_forecasts_post()",[170,802],{"x":199,"y":803,"width":771,"height":772,"rx":209,"fill":175,"stroke":210,"style":211},"152",[196,805,806],{"x":775,"y":231,"fill":194,"style":221},"derived operationId",[196,808,809],{"x":775,"y":235,"fill":200,"style":216},"createForecast",[238,811],{"x1":784,"y1":812,"x2":786,"y2":812,"stroke":194,"style":787},"187",[170,814],{"x":790,"y":803,"width":791,"height":772,"rx":209,"fill":250,"stroke":792,"style":793},[196,816,797],{"x":796,"y":231,"fill":194,"style":221},[196,818,42],{"x":796,"y":235,"fill":200,"style":216},[196,820,823],{"x":199,"y":821,"fill":194,"style":822},"250","text-anchor:start;font-size:11;font-family:var(--font-sans);","Renaming an operationId later breaks every regenerated SDK — choose before the first customer.",[53,825,827],{"id":826},"examples-tags-and-descriptions-the-renderers-honour","Examples, Tags, and Descriptions the Renderers Honour",[14,829,830,831,833,834,837,838,841,842,845,846,849,850,853],{},"Three parameters cover almost every documentation need, and builders reach for the wrong one constantly. Use ",[18,832,105],{}," on ",[18,835,836],{},"Body"," or ",[18,839,840],{},"Query"," for named request scenarios, ",[18,843,844],{},"Field(examples=[...])"," for single field-level values, and the ",[18,847,848],{},"responses"," mapping for named response examples. A bare ",[18,851,852],{},"example="," argument is deprecated and several generators drop it silently.",[14,855,856,857,859],{},"Tag metadata is separate from tags on routes. The ",[18,858,117],{}," list sets the order of the navigation sidebar and gives each group a Markdown blurb — this is prime real estate for telling a prospect what a group of endpoints costs.",[308,861,863],{"className":310,"code":862,"language":312,"meta":313,"style":313},"# app\u002Frouters\u002Fforecasts.py\nimport os\nfrom typing import Annotated\n\nfrom fastapi import APIRouter, Body, status\n\nfrom app.models import Forecast, ForecastRequest, ProblemDetail\n\nrouter = APIRouter(prefix=os.getenv(\"API_PREFIX\", \"\u002Fv1\"), tags=[\"Forecasts\"])\n\nSAMPLE_RESPONSE = {\n    \"forecast_id\": \"fc_01HZX9K2\",\n    \"sku\": \"SKU-1043\",\n    \"points\": [{\"period_start\": \"2026-08-03\", \"value\": 884.2}],\n    \"billable_units\": 1,\n}\n\n\n@router.post(\n    \"\u002Fforecasts\",\n    response_model=Forecast,\n    status_code=status.HTTP_201_CREATED,\n    summary=\"Create a demand forecast\",\n    responses={\n        201: {\n            \"content\": {\n                \"application\u002Fjson\": {\n                    \"examples\": {\n                        \"typical\": {\n                            \"summary\": \"Weekly retail forecast\",\n                            \"value\": SAMPLE_RESPONSE,\n                        }\n                    }\n                }\n            }\n        },\n        402: {\"model\": ProblemDetail, \"description\": \"Plan quota exhausted.\"},\n    },\n)\nasync def create_forecast(\n    payload: Annotated[\n        ForecastRequest,\n        Body(\n            openapi_examples={\n                \"weekly\": {\n                    \"summary\": \"Twelve weeks of history\",\n                    \"value\": {\"sku\": \"SKU-1043\", \"horizon\": 4, \"history\": [812, 790, 845]},\n                },\n                \"sparse\": {\n                    \"summary\": \"Intermittent demand\",\n                    \"value\": {\"sku\": \"SKU-9920\", \"horizon\": 8, \"history\": [0, 0, 3]},\n                },\n            }\n        ),\n    ],\n) -> Forecast:\n    return await run_forecast(payload)\n",[18,864,865,870,876,888,892,903,907,919,923,964,968,978,992,1004,1033,1045,1050,1054,1059,1068,1076,1087,1103,1116,1127,1136,1144,1152,1160,1168,1181,1193,1199,1205,1211,1217,1223,1249,1255,1260,1274,1280,1286,1292,1302,1310,1323,1372,1378,1386,1398,1440,1445,1450,1456,1462,1468],{"__ignoreMap":313},[317,866,867],{"class":238,"line":319},[317,868,869],{"class":322},"# app\u002Frouters\u002Fforecasts.py\n",[317,871,872,874],{"class":238,"line":326},[317,873,330],{"class":329},[317,875,334],{"class":333},[317,877,878,880,883,885],{"class":238,"line":337},[317,879,355],{"class":329},[317,881,882],{"class":333}," typing ",[317,884,330],{"class":329},[317,886,887],{"class":333}," Annotated\n",[317,889,890],{"class":238,"line":345},[317,891,349],{"emptyLinePlaceholder":348},[317,893,894,896,898,900],{"class":238,"line":352},[317,895,355],{"class":329},[317,897,628],{"class":333},[317,899,330],{"class":329},[317,901,902],{"class":333}," APIRouter, Body, status\n",[317,904,905],{"class":238,"line":366},[317,906,349],{"emptyLinePlaceholder":348},[317,908,909,911,914,916],{"class":238,"line":371},[317,910,355],{"class":329},[317,912,913],{"class":333}," app.models ",[317,915,330],{"class":329},[317,917,918],{"class":333}," Forecast, ForecastRequest, ProblemDetail\n",[317,920,921],{"class":238,"line":408},[317,922,349],{"emptyLinePlaceholder":348},[317,924,925,928,930,933,936,938,940,943,945,948,951,954,956,958,961],{"class":238,"line":413},[317,926,927],{"class":333},"router ",[317,929,449],{"class":329},[317,931,932],{"class":333}," APIRouter(",[317,934,935],{"class":682},"prefix",[317,937,449],{"class":329},[317,939,688],{"class":333},[317,941,942],{"class":387},"\"API_PREFIX\"",[317,944,488],{"class":333},[317,946,947],{"class":387},"\"\u002Fv1\"",[317,949,950],{"class":333},"), ",[317,952,953],{"class":682},"tags",[317,955,449],{"class":329},[317,957,391],{"class":333},[317,959,960],{"class":387},"\"Forecasts\"",[317,962,963],{"class":333},"])\n",[317,965,966],{"class":238,"line":418},[317,967,349],{"emptyLinePlaceholder":348},[317,969,970,973,975],{"class":238,"line":437},[317,971,972],{"class":374},"SAMPLE_RESPONSE",[317,974,378],{"class":329},[317,976,977],{"class":333}," {\n",[317,979,980,983,986,989],{"class":238,"line":443},[317,981,982],{"class":387},"    \"forecast_id\"",[317,984,985],{"class":333},": ",[317,987,988],{"class":387},"\"fc_01HZX9K2\"",[317,990,991],{"class":333},",\n",[317,993,994,997,999,1002],{"class":238,"line":476},[317,995,996],{"class":387},"    \"sku\"",[317,998,985],{"class":333},[317,1000,1001],{"class":387},"\"SKU-1043\"",[317,1003,991],{"class":333},[317,1005,1006,1009,1012,1015,1017,1020,1022,1025,1027,1030],{"class":238,"line":509},[317,1007,1008],{"class":387},"    \"points\"",[317,1010,1011],{"class":333},": [{",[317,1013,1014],{"class":387},"\"period_start\"",[317,1016,985],{"class":333},[317,1018,1019],{"class":387},"\"2026-08-03\"",[317,1021,488],{"class":333},[317,1023,1024],{"class":387},"\"value\"",[317,1026,985],{"class":333},[317,1028,1029],{"class":374},"884.2",[317,1031,1032],{"class":333},"}],\n",[317,1034,1035,1038,1040,1043],{"class":238,"line":537},[317,1036,1037],{"class":387},"    \"billable_units\"",[317,1039,985],{"class":333},[317,1041,1042],{"class":374},"1",[317,1044,991],{"class":333},[317,1046,1047],{"class":238,"line":554},[317,1048,1049],{"class":333},"}\n",[317,1051,1052],{"class":238,"line":570},[317,1053,349],{"emptyLinePlaceholder":348},[317,1055,1057],{"class":238,"line":1056},18,[317,1058,349],{"emptyLinePlaceholder":348},[317,1060,1062,1065],{"class":238,"line":1061},19,[317,1063,1064],{"class":424},"@router.post",[317,1066,1067],{"class":333},"(\n",[317,1069,1071,1074],{"class":238,"line":1070},20,[317,1072,1073],{"class":387},"    \"\u002Fforecasts\"",[317,1075,991],{"class":333},[317,1077,1079,1082,1084],{"class":238,"line":1078},21,[317,1080,1081],{"class":682},"    response_model",[317,1083,449],{"class":329},[317,1085,1086],{"class":333},"Forecast,\n",[317,1088,1090,1093,1095,1098,1101],{"class":238,"line":1089},22,[317,1091,1092],{"class":682},"    status_code",[317,1094,449],{"class":329},[317,1096,1097],{"class":333},"status.",[317,1099,1100],{"class":374},"HTTP_201_CREATED",[317,1102,991],{"class":333},[317,1104,1106,1109,1111,1114],{"class":238,"line":1105},23,[317,1107,1108],{"class":682},"    summary",[317,1110,449],{"class":329},[317,1112,1113],{"class":387},"\"Create a demand forecast\"",[317,1115,991],{"class":333},[317,1117,1119,1122,1124],{"class":238,"line":1118},24,[317,1120,1121],{"class":682},"    responses",[317,1123,449],{"class":329},[317,1125,1126],{"class":333},"{\n",[317,1128,1130,1133],{"class":238,"line":1129},25,[317,1131,1132],{"class":374},"        201",[317,1134,1135],{"class":333},": {\n",[317,1137,1139,1142],{"class":238,"line":1138},26,[317,1140,1141],{"class":387},"            \"content\"",[317,1143,1135],{"class":333},[317,1145,1147,1150],{"class":238,"line":1146},27,[317,1148,1149],{"class":387},"                \"application\u002Fjson\"",[317,1151,1135],{"class":333},[317,1153,1155,1158],{"class":238,"line":1154},28,[317,1156,1157],{"class":387},"                    \"examples\"",[317,1159,1135],{"class":333},[317,1161,1163,1166],{"class":238,"line":1162},29,[317,1164,1165],{"class":387},"                        \"typical\"",[317,1167,1135],{"class":333},[317,1169,1171,1174,1176,1179],{"class":238,"line":1170},30,[317,1172,1173],{"class":387},"                            \"summary\"",[317,1175,985],{"class":333},[317,1177,1178],{"class":387},"\"Weekly retail forecast\"",[317,1180,991],{"class":333},[317,1182,1184,1187,1189,1191],{"class":238,"line":1183},31,[317,1185,1186],{"class":387},"                            \"value\"",[317,1188,985],{"class":333},[317,1190,972],{"class":374},[317,1192,991],{"class":333},[317,1194,1196],{"class":238,"line":1195},32,[317,1197,1198],{"class":333},"                        }\n",[317,1200,1202],{"class":238,"line":1201},33,[317,1203,1204],{"class":333},"                    }\n",[317,1206,1208],{"class":238,"line":1207},34,[317,1209,1210],{"class":333},"                }\n",[317,1212,1214],{"class":238,"line":1213},35,[317,1215,1216],{"class":333},"            }\n",[317,1218,1220],{"class":238,"line":1219},36,[317,1221,1222],{"class":333},"        },\n",[317,1224,1226,1229,1232,1235,1238,1241,1243,1246],{"class":238,"line":1225},37,[317,1227,1228],{"class":374},"        402",[317,1230,1231],{"class":333},": {",[317,1233,1234],{"class":387},"\"model\"",[317,1236,1237],{"class":333},": ProblemDetail, ",[317,1239,1240],{"class":387},"\"description\"",[317,1242,985],{"class":333},[317,1244,1245],{"class":387},"\"Plan quota exhausted.\"",[317,1247,1248],{"class":333},"},\n",[317,1250,1252],{"class":238,"line":1251},38,[317,1253,1254],{"class":333},"    },\n",[317,1256,1258],{"class":238,"line":1257},39,[317,1259,405],{"class":333},[317,1261,1263,1266,1269,1272],{"class":238,"line":1262},40,[317,1264,1265],{"class":329},"async",[317,1267,1268],{"class":329}," def",[317,1270,1271],{"class":424}," create_forecast",[317,1273,1067],{"class":333},[317,1275,1277],{"class":238,"line":1276},41,[317,1278,1279],{"class":333},"    payload: Annotated[\n",[317,1281,1283],{"class":238,"line":1282},42,[317,1284,1285],{"class":333},"        ForecastRequest,\n",[317,1287,1289],{"class":238,"line":1288},43,[317,1290,1291],{"class":333},"        Body(\n",[317,1293,1295,1298,1300],{"class":238,"line":1294},44,[317,1296,1297],{"class":682},"            openapi_examples",[317,1299,449],{"class":329},[317,1301,1126],{"class":333},[317,1303,1305,1308],{"class":238,"line":1304},45,[317,1306,1307],{"class":387},"                \"weekly\"",[317,1309,1135],{"class":333},[317,1311,1313,1316,1318,1321],{"class":238,"line":1312},46,[317,1314,1315],{"class":387},"                    \"summary\"",[317,1317,985],{"class":333},[317,1319,1320],{"class":387},"\"Twelve weeks of history\"",[317,1322,991],{"class":333},[317,1324,1326,1329,1331,1334,1336,1338,1340,1343,1345,1348,1350,1353,1356,1359,1361,1364,1366,1369],{"class":238,"line":1325},47,[317,1327,1328],{"class":387},"                    \"value\"",[317,1330,1231],{"class":333},[317,1332,1333],{"class":387},"\"sku\"",[317,1335,985],{"class":333},[317,1337,1001],{"class":387},[317,1339,488],{"class":333},[317,1341,1342],{"class":387},"\"horizon\"",[317,1344,985],{"class":333},[317,1346,1347],{"class":374},"4",[317,1349,488],{"class":333},[317,1351,1352],{"class":387},"\"history\"",[317,1354,1355],{"class":333},": [",[317,1357,1358],{"class":374},"812",[317,1360,488],{"class":333},[317,1362,1363],{"class":374},"790",[317,1365,488],{"class":333},[317,1367,1368],{"class":374},"845",[317,1370,1371],{"class":333},"]},\n",[317,1373,1375],{"class":238,"line":1374},48,[317,1376,1377],{"class":333},"                },\n",[317,1379,1381,1384],{"class":238,"line":1380},49,[317,1382,1383],{"class":387},"                \"sparse\"",[317,1385,1135],{"class":333},[317,1387,1389,1391,1393,1396],{"class":238,"line":1388},50,[317,1390,1315],{"class":387},[317,1392,985],{"class":333},[317,1394,1395],{"class":387},"\"Intermittent demand\"",[317,1397,991],{"class":333},[317,1399,1401,1403,1405,1407,1409,1412,1414,1416,1418,1421,1423,1425,1427,1429,1431,1433,1435,1438],{"class":238,"line":1400},51,[317,1402,1328],{"class":387},[317,1404,1231],{"class":333},[317,1406,1333],{"class":387},[317,1408,985],{"class":333},[317,1410,1411],{"class":387},"\"SKU-9920\"",[317,1413,488],{"class":333},[317,1415,1342],{"class":387},[317,1417,985],{"class":333},[317,1419,1420],{"class":374},"8",[317,1422,488],{"class":333},[317,1424,1352],{"class":387},[317,1426,1355],{"class":333},[317,1428,172],{"class":374},[317,1430,488],{"class":333},[317,1432,172],{"class":374},[317,1434,488],{"class":333},[317,1436,1437],{"class":374},"3",[317,1439,1371],{"class":333},[317,1441,1443],{"class":238,"line":1442},52,[317,1444,1377],{"class":333},[317,1446,1448],{"class":238,"line":1447},53,[317,1449,1216],{"class":333},[317,1451,1453],{"class":238,"line":1452},54,[317,1454,1455],{"class":333},"        ),\n",[317,1457,1459],{"class":238,"line":1458},55,[317,1460,1461],{"class":333},"    ],\n",[317,1463,1465],{"class":238,"line":1464},56,[317,1466,1467],{"class":333},") -> Forecast:\n",[317,1469,1471,1473,1476],{"class":238,"line":1470},57,[317,1472,573],{"class":329},[317,1474,1475],{"class":329}," await",[317,1477,1478],{"class":333}," run_forecast(payload)\n",[14,1480,1481,1482,36],{},"Named examples pay for themselves the first afternoon. A visitor who can pick \"Intermittent demand\" from a dropdown and press execute understands your product in one click; one facing an empty JSON textarea closes the tab. Keep the payloads honest — they must validate against the model, or the try-it-out button returns a 422 and destroys the trust you just built. If your models are loose, tighten them first using the rules in ",[27,1483,1485],{"href":1484},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2\u002F","validating JSON with Pydantic v2",[53,1487,1489],{"id":1488},"hiding-internal-routes-without-losing-their-documentation","Hiding Internal Routes Without Losing Their Documentation",[14,1491,1492,1495,1496,36],{},[18,1493,1494],{},"include_in_schema=False"," removes a route from every document, including the one your own team reads. That is the wrong trade for admin tooling, replay endpoints, and migration hooks — you want those documented internally and invisible publicly. Build two artifacts from one route table by filtering routes before handing them to ",[18,1497,1498],{},"get_openapi",[308,1500,1502],{"className":310,"code":1501,"language":312,"meta":313,"style":313},"# app\u002Fspec.py\nimport os\n\nfrom fastapi import FastAPI\nfrom fastapi.openapi.utils import get_openapi\nfrom fastapi.routing import APIRoute\n\nTAG_METADATA = [\n    {\"name\": \"Forecasts\", \"description\": \"Billed per forecast created.\"},\n    {\"name\": \"Usage\", \"description\": \"Read consumption and remaining quota. Free.\"},\n]\n\n\ndef _is_public(route: APIRoute) -> bool:\n    private_prefix = os.getenv(\"INTERNAL_PATH_PREFIX\", \"\u002Finternal\")\n    private_tag = os.getenv(\"INTERNAL_TAG\", \"Internal\")\n    return not route.path.startswith(private_prefix) and private_tag not in (route.tags or [])\n\n\ndef build_schema(app: FastAPI, audience: str) -> dict:\n    routes = [r for r in app.routes if not isinstance(r, APIRoute) or _is_public(r)]\n    match audience:\n        case \"public\":\n            selected, title_suffix = routes, \"\"\n        case \"internal\":\n            selected, title_suffix = app.routes, \" (internal)\"\n        case _:\n            raise ValueError(f\"unknown audience: {audience}\")\n    return get_openapi(\n        title=os.getenv(\"API_TITLE\", \"Untitled API\") + title_suffix,\n        version=os.getenv(\"API_VERSION\", \"0.0.0\"),\n        routes=selected,\n        tags=TAG_METADATA,\n        servers=[{\"url\": os.getenv(\"API_PUBLIC_URL\", \"http:\u002F\u002Flocalhost:8000\")}],\n    )\n",[18,1503,1504,1509,1515,1519,1529,1541,1551,1555,1565,1588,1610,1614,1618,1622,1636,1655,1674,1705,1709,1713,1733,1768,1776,1786,1799,1808,1820,1827,1857,1864,1886,1902,1912,1923,1950],{"__ignoreMap":313},[317,1505,1506],{"class":238,"line":319},[317,1507,1508],{"class":322},"# app\u002Fspec.py\n",[317,1510,1511,1513],{"class":238,"line":326},[317,1512,330],{"class":329},[317,1514,334],{"class":333},[317,1516,1517],{"class":238,"line":337},[317,1518,349],{"emptyLinePlaceholder":348},[317,1520,1521,1523,1525,1527],{"class":238,"line":345},[317,1522,355],{"class":329},[317,1524,628],{"class":333},[317,1526,330],{"class":329},[317,1528,633],{"class":333},[317,1530,1531,1533,1536,1538],{"class":238,"line":352},[317,1532,355],{"class":329},[317,1534,1535],{"class":333}," fastapi.openapi.utils ",[317,1537,330],{"class":329},[317,1539,1540],{"class":333}," get_openapi\n",[317,1542,1543,1545,1547,1549],{"class":238,"line":366},[317,1544,355],{"class":329},[317,1546,358],{"class":333},[317,1548,330],{"class":329},[317,1550,363],{"class":333},[317,1552,1553],{"class":238,"line":371},[317,1554,349],{"emptyLinePlaceholder":348},[317,1556,1557,1560,1562],{"class":238,"line":408},[317,1558,1559],{"class":374},"TAG_METADATA",[317,1561,378],{"class":329},[317,1563,1564],{"class":333}," [\n",[317,1566,1567,1570,1573,1575,1577,1579,1581,1583,1586],{"class":238,"line":413},[317,1568,1569],{"class":333},"    {",[317,1571,1572],{"class":387},"\"name\"",[317,1574,985],{"class":333},[317,1576,960],{"class":387},[317,1578,488],{"class":333},[317,1580,1240],{"class":387},[317,1582,985],{"class":333},[317,1584,1585],{"class":387},"\"Billed per forecast created.\"",[317,1587,1248],{"class":333},[317,1589,1590,1592,1594,1596,1599,1601,1603,1605,1608],{"class":238,"line":418},[317,1591,1569],{"class":333},[317,1593,1572],{"class":387},[317,1595,985],{"class":333},[317,1597,1598],{"class":387},"\"Usage\"",[317,1600,488],{"class":333},[317,1602,1240],{"class":387},[317,1604,985],{"class":333},[317,1606,1607],{"class":387},"\"Read consumption and remaining quota. Free.\"",[317,1609,1248],{"class":333},[317,1611,1612],{"class":238,"line":437},[317,1613,534],{"class":333},[317,1615,1616],{"class":238,"line":443},[317,1617,349],{"emptyLinePlaceholder":348},[317,1619,1620],{"class":238,"line":476},[317,1621,349],{"emptyLinePlaceholder":348},[317,1623,1624,1626,1629,1631,1634],{"class":238,"line":509},[317,1625,421],{"class":329},[317,1627,1628],{"class":424}," _is_public",[317,1630,428],{"class":333},[317,1632,1633],{"class":374},"bool",[317,1635,434],{"class":333},[317,1637,1638,1641,1643,1645,1648,1650,1653],{"class":238,"line":537},[317,1639,1640],{"class":333},"    private_prefix ",[317,1642,449],{"class":329},[317,1644,482],{"class":333},[317,1646,1647],{"class":387},"\"INTERNAL_PATH_PREFIX\"",[317,1649,488],{"class":333},[317,1651,1652],{"class":387},"\"\u002Finternal\"",[317,1654,405],{"class":333},[317,1656,1657,1660,1662,1664,1667,1669,1672],{"class":238,"line":554},[317,1658,1659],{"class":333},"    private_tag ",[317,1661,449],{"class":329},[317,1663,482],{"class":333},[317,1665,1666],{"class":387},"\"INTERNAL_TAG\"",[317,1668,488],{"class":333},[317,1670,1671],{"class":387},"\"Internal\"",[317,1673,405],{"class":333},[317,1675,1676,1678,1681,1684,1687,1690,1693,1696,1699,1702],{"class":238,"line":570},[317,1677,573],{"class":329},[317,1679,1680],{"class":329}," not",[317,1682,1683],{"class":333}," route.path.startswith(private_prefix) ",[317,1685,1686],{"class":329},"and",[317,1688,1689],{"class":333}," private_tag ",[317,1691,1692],{"class":329},"not",[317,1694,1695],{"class":329}," in",[317,1697,1698],{"class":333}," (route.tags ",[317,1700,1701],{"class":329},"or",[317,1703,1704],{"class":333}," [])\n",[317,1706,1707],{"class":238,"line":1056},[317,1708,349],{"emptyLinePlaceholder":348},[317,1710,1711],{"class":238,"line":1061},[317,1712,349],{"emptyLinePlaceholder":348},[317,1714,1715,1717,1720,1723,1725,1728,1731],{"class":238,"line":1070},[317,1716,421],{"class":329},[317,1718,1719],{"class":424}," build_schema",[317,1721,1722],{"class":333},"(app: FastAPI, audience: ",[317,1724,431],{"class":374},[317,1726,1727],{"class":333},") -> ",[317,1729,1730],{"class":374},"dict",[317,1732,434],{"class":333},[317,1734,1735,1738,1740,1743,1745,1748,1750,1753,1755,1757,1760,1763,1765],{"class":238,"line":1078},[317,1736,1737],{"class":333},"    routes ",[317,1739,449],{"class":329},[317,1741,1742],{"class":333}," [r ",[317,1744,455],{"class":329},[317,1746,1747],{"class":333}," r ",[317,1749,461],{"class":329},[317,1751,1752],{"class":333}," app.routes ",[317,1754,470],{"class":329},[317,1756,1680],{"class":329},[317,1758,1759],{"class":374}," isinstance",[317,1761,1762],{"class":333},"(r, APIRoute) ",[317,1764,1701],{"class":329},[317,1766,1767],{"class":333}," _is_public(r)]\n",[317,1769,1770,1773],{"class":238,"line":1089},[317,1771,1772],{"class":329},"    match",[317,1774,1775],{"class":333}," audience:\n",[317,1777,1778,1781,1784],{"class":238,"line":1105},[317,1779,1780],{"class":329},"        case",[317,1782,1783],{"class":387}," \"public\"",[317,1785,434],{"class":333},[317,1787,1788,1791,1793,1796],{"class":238,"line":1118},[317,1789,1790],{"class":333},"            selected, title_suffix ",[317,1792,449],{"class":329},[317,1794,1795],{"class":333}," routes, ",[317,1797,1798],{"class":387},"\"\"\n",[317,1800,1801,1803,1806],{"class":238,"line":1129},[317,1802,1780],{"class":329},[317,1804,1805],{"class":387}," \"internal\"",[317,1807,434],{"class":333},[317,1809,1810,1812,1814,1817],{"class":238,"line":1138},[317,1811,1790],{"class":333},[317,1813,449],{"class":329},[317,1815,1816],{"class":333}," app.routes, ",[317,1818,1819],{"class":387},"\" (internal)\"\n",[317,1821,1822,1824],{"class":238,"line":1146},[317,1823,1780],{"class":329},[317,1825,1826],{"class":333}," _:\n",[317,1828,1829,1832,1835,1838,1841,1844,1847,1850,1853,1855],{"class":238,"line":1154},[317,1830,1831],{"class":329},"            raise",[317,1833,1834],{"class":374}," ValueError",[317,1836,1837],{"class":333},"(",[317,1839,1840],{"class":329},"f",[317,1842,1843],{"class":387},"\"unknown audience: ",[317,1845,1846],{"class":374},"{",[317,1848,1849],{"class":333},"audience",[317,1851,1852],{"class":374},"}",[317,1854,388],{"class":387},[317,1856,405],{"class":333},[317,1858,1859,1861],{"class":238,"line":1162},[317,1860,573],{"class":329},[317,1862,1863],{"class":333}," get_openapi(\n",[317,1865,1866,1868,1870,1872,1874,1876,1878,1881,1883],{"class":238,"line":1170},[317,1867,683],{"class":682},[317,1869,449],{"class":329},[317,1871,688],{"class":333},[317,1873,691],{"class":387},[317,1875,488],{"class":333},[317,1877,696],{"class":387},[317,1879,1880],{"class":333},") ",[317,1882,400],{"class":329},[317,1884,1885],{"class":333}," title_suffix,\n",[317,1887,1888,1890,1892,1894,1896,1898,1900],{"class":238,"line":1183},[317,1889,704],{"class":682},[317,1891,449],{"class":329},[317,1893,688],{"class":333},[317,1895,711],{"class":387},[317,1897,488],{"class":333},[317,1899,716],{"class":387},[317,1901,699],{"class":333},[317,1903,1904,1907,1909],{"class":238,"line":1195},[317,1905,1906],{"class":682},"        routes",[317,1908,449],{"class":329},[317,1910,1911],{"class":333},"selected,\n",[317,1913,1914,1917,1919,1921],{"class":238,"line":1201},[317,1915,1916],{"class":682},"        tags",[317,1918,449],{"class":329},[317,1920,1559],{"class":374},[317,1922,991],{"class":333},[317,1924,1925,1928,1930,1933,1936,1939,1942,1944,1947],{"class":238,"line":1207},[317,1926,1927],{"class":682},"        servers",[317,1929,449],{"class":329},[317,1931,1932],{"class":333},"[{",[317,1934,1935],{"class":387},"\"url\"",[317,1937,1938],{"class":333},": os.getenv(",[317,1940,1941],{"class":387},"\"API_PUBLIC_URL\"",[317,1943,488],{"class":333},[317,1945,1946],{"class":387},"\"http:\u002F\u002Flocalhost:8000\"",[317,1948,1949],{"class":333},")}],\n",[317,1951,1952],{"class":238,"line":1213},[317,1953,733],{"class":333},[14,1955,1956,1957,1960,1961,1964,1965,1967],{},"Now ",[18,1958,1959],{},"build_schema(app, \"public\")"," feeds the file you publish and ",[18,1962,1963],{},"build_schema(app, \"internal\")"," feeds the copy your team browses on a private host. The filter is data-driven, so adding an internal route is a matter of tagging it rather than remembering a decorator argument. One caution: filtering hides the path but not always the schemas those paths referenced, because ",[18,1966,1498],{}," prunes components it no longer needs only when nothing else refers to them. Assert on the published document in CI rather than trusting the filter.",[151,1969,160,1974,160,1977,160,1980,160,1982,160,1989,160,1993,160,1997,160,2001,160,2006,160,2010,160,2014,160,2019,160,2023,160,2026,160,2031,160,2035,160,2039,160,2042,160,2046,160,2049],{"viewBox":1970,"role":154,"ariaLabelledBy":1971,"xmlns":158,"style":159},"0 0 760 300",[1972,1973],"cfos-split-t","cfos-split-d",[162,1975,1976],{"id":1972},"One route table producing a public and an internal specification",[166,1978,1979],{"id":1973},"A route filter splits the application route table into a published customer artifact without internal paths and a complete internal artifact kept on a private host.",[170,1981],{"x":172,"y":172,"width":173,"height":771,"fill":175},[177,1983,179,1984,160],{},[181,1985,189,1987,179],{"id":1986,"viewBox":184,"refX":185,"refY":186,"markerWidth":187,"markerHeight":187,"orient":188},"cfos-split-arrow",[191,1988],{"d":193,"fill":194},[170,1990],{"x":755,"y":1991,"width":1992,"height":770,"rx":209,"fill":175,"stroke":251,"style":211},"26","220",[196,1994,1996],{"x":198,"y":1995,"fill":200,"style":216},"50","app.routes",[196,1998,2000],{"x":198,"y":1999,"fill":194,"style":221},"72","18 routes, 4 tagged Internal",[170,2002],{"x":2003,"y":2004,"width":775,"height":2005,"rx":209,"fill":250,"stroke":792,"style":793},"290","112","46",[196,2007,2009],{"x":198,"y":2008,"fill":200,"style":216},"140","route filter",[238,2011],{"x1":198,"y1":2012,"x2":198,"y2":780,"stroke":194,"style":2013},"88","stroke-width:2;marker-end:url(#cfos-split-arrow);",[238,2015],{"x1":2016,"y1":2017,"x2":207,"y2":2018,"stroke":194,"style":2013},"330","160","196",[238,2020],{"x1":2021,"y1":2017,"x2":2022,"y2":2018,"stroke":194,"style":2013},"430","560",[170,2024],{"x":2005,"y":2025,"width":755,"height":215,"rx":209,"fill":175,"stroke":210,"style":211},"198",[196,2027,2030],{"x":2028,"y":2029,"fill":200,"style":216},"181","224","openapi-public.json",[196,2032,2034],{"x":2028,"y":2033,"fill":194,"style":221},"246","14 operations, CDN cached",[196,2036,2038],{"x":2028,"y":2037,"fill":194,"style":221},"266","feeds customer SDKs",[170,2040],{"x":2041,"y":2025,"width":755,"height":215,"rx":209,"fill":175,"stroke":269,"style":211},"444",[196,2043,2045],{"x":2044,"y":2029,"fill":200,"style":216},"579","openapi-internal.json",[196,2047,2048],{"x":2044,"y":2033,"fill":194,"style":221},"18 operations, private host",[196,2050,2051],{"x":2044,"y":2037,"fill":194,"style":221},"feeds on-call runbooks",[53,2053,2055],{"id":2054},"security-schemes-a-generated-client-will-actually-send","Security Schemes a Generated Client Will Actually Send",[14,2057,2058,2059,2062,2063,2066,2067,2070,2071,2074,2075,2078,2079,2082],{},"A generated client sends credentials only when the document declares a security scheme and an operation references it. FastAPI writes ",[18,2060,2061],{},"components.securitySchemes"," automatically from any ",[18,2064,2065],{},"fastapi.security"," class you use as a dependency, which is the whole reason to use them instead of reading a header manually. Declaring ",[18,2068,2069],{},"APIKeyHeader"," gives generators the header name; ",[18,2072,2073],{},"HTTPBearer"," gives them the ",[18,2076,2077],{},"Authorization: Bearer"," shape; ",[18,2080,2081],{},"OAuth2AuthorizationCodeBearer"," gives them the token URL to run a real flow against.",[14,2084,2085],{},"What FastAPI will not do is set a document-level default or mark specific operations as public. Add both in the post-processor, because a spec that requires auth on your health probe sends customers into a debugging loop before they ever get a key.",[308,2087,2089],{"className":310,"code":2088,"language":312,"meta":313,"style":313},"# app\u002Fsecurity_doc.py\nimport os\n\nPUBLIC_OPERATION_IDS = {\"healthLive\", \"healthReady\"}\n\n\ndef apply_security(schema: dict) -> dict:\n    scheme = os.getenv(\"API_SECURITY_SCHEME_NAME\", \"ApiKeyAuth\")\n    schema.setdefault(\"components\", {}).setdefault(\"securitySchemes\", {}).setdefault(\n        scheme,\n        {\n            \"type\": \"apiKey\",\n            \"in\": \"header\",\n            \"name\": os.getenv(\"API_KEY_HEADER\", \"X-API-Key\"),\n            \"description\": \"Issued in the dashboard. Rotate without downtime.\",\n        },\n    )\n    schema[\"security\"] = [{scheme: []}]\n    for path_item in schema.get(\"paths\", {}).values():\n        for operation in path_item.values():\n            if operation.get(\"operationId\") in PUBLIC_OPERATION_IDS:\n                operation[\"security\"] = []\n            operation.setdefault(\"x-codeSamples\", [])\n    return schema\n",[18,2090,2091,2096,2102,2106,2126,2130,2134,2152,2171,2188,2193,2198,2210,2222,2239,2251,2255,2259,2275,2294,2307,2327,2341,2352],{"__ignoreMap":313},[317,2092,2093],{"class":238,"line":319},[317,2094,2095],{"class":322},"# app\u002Fsecurity_doc.py\n",[317,2097,2098,2100],{"class":238,"line":326},[317,2099,330],{"class":329},[317,2101,334],{"class":333},[317,2103,2104],{"class":238,"line":337},[317,2105,349],{"emptyLinePlaceholder":348},[317,2107,2108,2111,2113,2116,2119,2121,2124],{"class":238,"line":345},[317,2109,2110],{"class":374},"PUBLIC_OPERATION_IDS",[317,2112,378],{"class":329},[317,2114,2115],{"class":333}," {",[317,2117,2118],{"class":387},"\"healthLive\"",[317,2120,488],{"class":333},[317,2122,2123],{"class":387},"\"healthReady\"",[317,2125,1049],{"class":333},[317,2127,2128],{"class":238,"line":352},[317,2129,349],{"emptyLinePlaceholder":348},[317,2131,2132],{"class":238,"line":366},[317,2133,349],{"emptyLinePlaceholder":348},[317,2135,2136,2138,2141,2144,2146,2148,2150],{"class":238,"line":371},[317,2137,421],{"class":329},[317,2139,2140],{"class":424}," apply_security",[317,2142,2143],{"class":333},"(schema: ",[317,2145,1730],{"class":374},[317,2147,1727],{"class":333},[317,2149,1730],{"class":374},[317,2151,434],{"class":333},[317,2153,2154,2157,2159,2161,2164,2166,2169],{"class":238,"line":408},[317,2155,2156],{"class":333},"    scheme ",[317,2158,449],{"class":329},[317,2160,482],{"class":333},[317,2162,2163],{"class":387},"\"API_SECURITY_SCHEME_NAME\"",[317,2165,488],{"class":333},[317,2167,2168],{"class":387},"\"ApiKeyAuth\"",[317,2170,405],{"class":333},[317,2172,2173,2176,2179,2182,2185],{"class":238,"line":413},[317,2174,2175],{"class":333},"    schema.setdefault(",[317,2177,2178],{"class":387},"\"components\"",[317,2180,2181],{"class":333},", {}).setdefault(",[317,2183,2184],{"class":387},"\"securitySchemes\"",[317,2186,2187],{"class":333},", {}).setdefault(\n",[317,2189,2190],{"class":238,"line":418},[317,2191,2192],{"class":333},"        scheme,\n",[317,2194,2195],{"class":238,"line":437},[317,2196,2197],{"class":333},"        {\n",[317,2199,2200,2203,2205,2208],{"class":238,"line":443},[317,2201,2202],{"class":387},"            \"type\"",[317,2204,985],{"class":333},[317,2206,2207],{"class":387},"\"apiKey\"",[317,2209,991],{"class":333},[317,2211,2212,2215,2217,2220],{"class":238,"line":476},[317,2213,2214],{"class":387},"            \"in\"",[317,2216,985],{"class":333},[317,2218,2219],{"class":387},"\"header\"",[317,2221,991],{"class":333},[317,2223,2224,2227,2229,2232,2234,2237],{"class":238,"line":509},[317,2225,2226],{"class":387},"            \"name\"",[317,2228,1938],{"class":333},[317,2230,2231],{"class":387},"\"API_KEY_HEADER\"",[317,2233,488],{"class":333},[317,2235,2236],{"class":387},"\"X-API-Key\"",[317,2238,699],{"class":333},[317,2240,2241,2244,2246,2249],{"class":238,"line":537},[317,2242,2243],{"class":387},"            \"description\"",[317,2245,985],{"class":333},[317,2247,2248],{"class":387},"\"Issued in the dashboard. Rotate without downtime.\"",[317,2250,991],{"class":333},[317,2252,2253],{"class":238,"line":554},[317,2254,1222],{"class":333},[317,2256,2257],{"class":238,"line":570},[317,2258,733],{"class":333},[317,2260,2261,2264,2267,2270,2272],{"class":238,"line":1056},[317,2262,2263],{"class":333},"    schema[",[317,2265,2266],{"class":387},"\"security\"",[317,2268,2269],{"class":333},"] ",[317,2271,449],{"class":329},[317,2273,2274],{"class":333}," [{scheme: []}]\n",[317,2276,2277,2280,2283,2285,2288,2291],{"class":238,"line":1061},[317,2278,2279],{"class":329},"    for",[317,2281,2282],{"class":333}," path_item ",[317,2284,461],{"class":329},[317,2286,2287],{"class":333}," schema.get(",[317,2289,2290],{"class":387},"\"paths\"",[317,2292,2293],{"class":333},", {}).values():\n",[317,2295,2296,2299,2302,2304],{"class":238,"line":1070},[317,2297,2298],{"class":329},"        for",[317,2300,2301],{"class":333}," operation ",[317,2303,461],{"class":329},[317,2305,2306],{"class":333}," path_item.values():\n",[317,2308,2309,2312,2315,2318,2320,2322,2325],{"class":238,"line":1078},[317,2310,2311],{"class":329},"            if",[317,2313,2314],{"class":333}," operation.get(",[317,2316,2317],{"class":387},"\"operationId\"",[317,2319,1880],{"class":333},[317,2321,461],{"class":329},[317,2323,2324],{"class":374}," PUBLIC_OPERATION_IDS",[317,2326,434],{"class":333},[317,2328,2329,2332,2334,2336,2338],{"class":238,"line":1089},[317,2330,2331],{"class":333},"                operation[",[317,2333,2266],{"class":387},[317,2335,2269],{"class":333},[317,2337,449],{"class":329},[317,2339,2340],{"class":333}," []\n",[317,2342,2343,2346,2349],{"class":238,"line":1105},[317,2344,2345],{"class":333},"            operation.setdefault(",[317,2347,2348],{"class":387},"\"x-codeSamples\"",[317,2350,2351],{"class":333},", [])\n",[317,2353,2354,2356],{"class":238,"line":1118},[317,2355,573],{"class":329},[317,2357,2358],{"class":333}," schema\n",[14,2360,2361,2362,2365,2366,2369,2370,2374,2375,36],{},"An empty ",[18,2363,2364],{},"security"," array on an operation is the OpenAPI way to say \"this one needs nothing\", and every serious generator honours it. The ",[18,2367,2368],{},"x-codeSamples"," key is a ReDoc vendor extension where you can inject curl and Python snippets per operation — a post-processing job precisely because FastAPI has no decorator for it. Which token style you actually issue is a separate decision, weighed in ",[27,2371,2373],{"href":2372},"\u002Fgetting-started-with-python-apis-for-builders\u002Fhandling-api-authentication-in-python\u002Fjwt-vs-api-keys-for-python-apis\u002F","JWT vs API keys for Python APIs","; if you went the OAuth route, the scheme is generated by the flow described in ",[27,2376,2378],{"href":2377},"\u002Fgetting-started-with-python-apis-for-builders\u002Fhandling-api-authentication-in-python\u002Fimplementing-oauth2-authorization-code-flow-in-fastapi\u002F","implementing the OAuth2 authorization code flow in FastAPI",[151,2380,160,2385,160,2388,160,2391,160,2393,160,2399,160,2403,160,2407,160,2411,160,2413,160,2416,160,2419,160,2422,160,2425,160,2428,160,2430,160,2432,160,2435,160,2437,160,2440,160,2443,160,2447,160,2449,160,2452,160,2454],{"viewBox":2381,"role":154,"ariaLabelledBy":2382,"xmlns":158,"style":159},"0 0 760 260",[2383,2384],"cfos-sec-t","cfos-sec-d",[162,2386,2387],{"id":2383},"Security scheme classes and the credential a generated client sends",[166,2389,2390],{"id":2384},"Each FastAPI security class emits a different security scheme object, which determines whether the generated client sends a custom header, a bearer token, or runs a full authorization flow.",[170,2392],{"x":172,"y":172,"width":173,"height":242,"fill":175},[196,2394,2398],{"x":2395,"y":2396,"fill":200,"style":2397},"150","34","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Dependency class",[196,2400,2402],{"x":2401,"y":2396,"fill":200,"style":2397},"400","Emitted scheme",[196,2404,2406],{"x":2405,"y":2396,"fill":200,"style":2397},"630","Client behaviour",[170,2408],{"x":205,"y":206,"width":2409,"height":2410,"rx":1420,"fill":175,"stroke":210,"style":211},"252","54",[196,2412,2069],{"x":2395,"y":249,"fill":200,"style":216},[170,2414],{"x":2415,"y":206,"width":2029,"height":2410,"rx":1420,"fill":250,"stroke":792,"style":793},"288",[196,2417,2418],{"x":2401,"y":249,"fill":200,"style":216},"type: apiKey, in: header",[170,2420],{"x":2421,"y":206,"width":268,"height":2410,"rx":1420,"fill":175,"stroke":792,"style":793},"524",[196,2423,2424],{"x":2405,"y":249,"fill":194,"style":221},"sends X-API-Key",[170,2426],{"x":205,"y":2427,"width":2409,"height":2410,"rx":1420,"fill":175,"stroke":251,"style":211},"118",[196,2429,2073],{"x":2395,"y":2395,"fill":200,"style":216},[170,2431],{"x":2415,"y":2427,"width":2029,"height":2410,"rx":1420,"fill":250,"stroke":792,"style":793},[196,2433,2434],{"x":2401,"y":2395,"fill":200,"style":216},"type: http, scheme: bearer",[170,2436],{"x":2421,"y":2427,"width":268,"height":2410,"rx":1420,"fill":175,"stroke":792,"style":793},[196,2438,2439],{"x":2405,"y":2395,"fill":194,"style":221},"sends Authorization",[170,2441],{"x":205,"y":2442,"width":2409,"height":2410,"rx":1420,"fill":175,"stroke":269,"style":211},"184",[196,2444,2446],{"x":2395,"y":2445,"fill":200,"style":216},"216","OAuth2AuthorizationCode",[170,2448],{"x":2415,"y":2442,"width":2029,"height":2410,"rx":1420,"fill":250,"stroke":792,"style":793},[196,2450,2451],{"x":2401,"y":2445,"fill":200,"style":216},"type: oauth2, flows",[170,2453],{"x":2421,"y":2442,"width":268,"height":2410,"rx":1420,"fill":175,"stroke":792,"style":793},[196,2455,2456],{"x":2405,"y":2445,"fill":194,"style":221},"runs token exchange",[53,2458,2460],{"id":2459},"when-to-post-process-and-when-to-refuse","When to Post-Process, and When to Refuse",[14,2462,2463],{},"Post-process when the field you need has no FastAPI parameter, when you must publish a filtered artifact, or when a marketplace demands a vendor extension. Those are legitimate and permanent needs. Refuse when you are papering over a modelling problem: rewriting a schema in the post-processor so it matches what the endpoint actually returns produces a document that lies the moment someone changes the code, and generated clients fail at runtime rather than at build time.",[14,2465,2466,2467,2470],{},"Stage matters too. Before your first external customer, do everything declaratively and skip the post-processor entirely — you will change route names weekly and a mutation script keyed on operation IDs will break constantly. Once you have paying integrations, a fifty-line post-processor plus a contract test costs nothing per request, since the document builds once per worker and caches on ",[18,2468,2469],{},"app.openapi_schema",". At high traffic the calculation does not change, because the spec should be a static file behind a CDN by then anyway. The only real cost is maintenance: every mutation keyed on a string is a future silent no-op, so assert on the result.",[53,2472,2474],{"id":2473},"migrating-an-existing-api-in-five-steps","Migrating an Existing API in Five Steps",[14,2476,2477,2478,2480,2481,2483,2484,2486,2487,2491],{},"Do this in one afternoon and ship it behind a version bump. First, commit today's ",[18,2479,20],{}," as a baseline so you can diff every subsequent change. Second, add ",[18,2482,305],{}," and regenerate — expect every operation ID to change, which is exactly why you do it before customers generate clients rather than after. Third, add ",[18,2485,117],{}," and route tags, then reorder the list until the sidebar reads like a product tour. Fourth, tag internal routes and swap in the two-artifact build. Fifth, add a contract test that asserts operation IDs are unique, that public operations carry a security requirement and health does not, and that no path starts with your internal prefix. That test belongs in the same job described in ",[27,2488,2490],{"href":2489},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002F","testing Python APIs with pytest",", and it is the only thing that stops the next contributor from undoing all of this.",[14,2493,2494,2495,36],{},"If customers already generate clients from your current document, treat the operation ID rename as a breaking change and follow the notice period in ",[27,2496,2498],{"href":2497},"\u002Fscaling-and-operating-production-python-apis\u002Fversioning-and-evolving-public-apis\u002Fdeprecating-an-api-endpoint-without-breaking-customers\u002F","deprecating an API endpoint without breaking customers",[53,2500,2502],{"id":2501},"builder-verdict","Builder Verdict",[14,2504,2505,2506,2508],{},"The declarative layer wins, and it is not close. Route decorators, Pydantic models, and a single ",[18,2507,305],{}," cover roughly ninety percent of what makes a spec sellable, and they cannot drift from runtime behaviour because they define runtime behaviour. Keep the post-processor small and boring: document-level security, public-operation exemptions, vendor extensions, and the public-versus-internal split. That is four responsibilities, fifty lines, and one test file.",[14,2510,2511],{},"Judge the result commercially. A clean document turns an evaluation into an integration in under ten minutes, which is the single cheapest conversion improvement available to a small API business — cheaper than a pricing page rewrite and far cheaper than the support hours a bad spec generates forever. Ship the declarative pass this week, add the post-processor when a real requirement forces it, and never mutate a schema to hide a modelling mistake.",[53,2513,2515],{"id":2514},"faq","FAQ",[14,2517,2518,2522],{},[2519,2520,2521],"strong",{},"Does renaming operation IDs cost me existing customers?","\nIt breaks their generated SDKs the next time they regenerate, not their live HTTP calls, so nothing fails overnight. The cost is a support conversation per integrated customer, which is why you renormalise identifiers before your first paying integration and treat any later change as a major version with a notice window.",[14,2524,2525,2528],{},[2519,2526,2527],{},"What does a customized spec cost to serve at a million documentation hits a month?","\nEffectively nothing if you export it to a static file and serve it from object storage or a CDN, and forty to a hundred dollars of egress plus real worker contention if you serve a 470 KB document from your API process. The customization itself is free at runtime because the document builds once per worker and caches.",[14,2530,2531,2534,2535,2537],{},[2519,2532,2533],{},"Can I rotate the API key header name in the spec without breaking clients?","\nNo. The header name is baked into every generated client, so changing ",[18,2536,46],{}," silently strips credentials from requests already in production. Accept both headers server-side for a full deprecation window, publish the new one in the scheme description first, and only then flip the documented value.",[14,2539,2540,2543],{},[2519,2541,2542],{},"Is a post-processor risky to maintain as the API grows?","\nOnly if it is unasserted. Every mutation keyed on an operation ID or path string becomes a silent no-op the day someone renames the target, so pair each rule with a test that fails when it stops matching. Fifty tested lines are safe; three hundred untested ones will publish a wrong document eventually.",[14,2545,2546,2549],{},[2519,2547,2548],{},"Should the internal spec live in the same repository as the public one?","\nYes, same repository, different published destination. Both artifacts come from one route table, so they cannot drift, and committing both means a diff shows you precisely when a route crossed from internal to public — which is exactly the review moment where an accidental exposure gets caught.",[53,2551,2553],{"id":2552},"related","Related",[2555,2556,2557,2563,2570,2577,2584],"ul",{},[2558,2559,2560,2562],"li",{},[27,2561,30],{"href":29}," — the parent guide covering metadata, spec export, and publishing the artifact this page customizes.",[2558,2564,2565,2569],{},[27,2566,2568],{"href":2567},"\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fredoc-vs-swagger-ui\u002F","ReDoc vs Swagger UI"," — decide which renderer displays your tags, examples, and vendor extensions best.",[2558,2571,2572,2576],{},[27,2573,2575],{"href":2574},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002F","Setting Up FastAPI"," — the application factory and router layout every snippet here assumes.",[2558,2578,2579,2583],{},[27,2580,2582],{"href":2581},"\u002Fgetting-started-with-python-apis-for-builders\u002Fhandling-api-authentication-in-python\u002F","Handling API Authentication in Python"," — implement the credentials your security scheme now advertises to generated clients.",[2558,2585,2586,2590],{},[27,2587,2589],{"href":2588},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fcreating-a-developer-portal-for-your-api\u002F","Creating a Developer Portal for Your API"," — wrap the customized spec in a self-serve signup and key-issuing flow.",[2592,2593,2594],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":313,"searchDepth":326,"depth":326,"links":2596},[2597,2598,2599,2600,2601,2602,2603,2604,2605,2606],{"id":55,"depth":326,"text":56},{"id":295,"depth":326,"text":296},{"id":826,"depth":326,"text":827},{"id":1488,"depth":326,"text":1489},{"id":2054,"depth":326,"text":2055},{"id":2459,"depth":326,"text":2460},{"id":2473,"depth":326,"text":2474},{"id":2501,"depth":326,"text":2502},{"id":2514,"depth":326,"text":2515},{"id":2552,"depth":326,"text":2553},"Override FastAPI's generated OpenAPI document: clean operation IDs, named examples, tag metadata, hidden internal routes, and security schemes SDKs honour.","md",{"pageTitle":2610,"type":2611,"datePublished":2612,"dateModified":2612},"Customizing the FastAPI OpenAPI Schema for Real Clients","article","2026-07-23","\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fcustomizing-fastapi-openapi-schema",{"title":5,"description":2607},"getting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fcustomizing-fastapi-openapi-schema\u002Findex","VsOb0bEUEjYsSn6V4BFnglNtYBwJlPsNcOVIVUd-WTk",{"@context":2618,"@type":2619,"mainEntity":2620},"https:\u002F\u002Fschema.org","FAQPage",[2621,2626,2629,2632,2635],{"@type":2622,"name":2521,"acceptedAnswer":2623},"Question",{"@type":2624,"text":2625},"Answer","It breaks their generated SDKs the next time they regenerate, not their live HTTP calls, so nothing fails overnight. The cost is a support conversation per integrated customer, which is why you renormalise identifiers before your first paying integration and treat any later change as a major version with a notice window.",{"@type":2622,"name":2527,"acceptedAnswer":2627},{"@type":2624,"text":2628},"Effectively nothing if you export it to a static file and serve it from object storage or a CDN, and forty to a hundred dollars of egress plus real worker contention if you serve a 470 KB document from your API process. The customization itself is free at runtime because the document builds once per worker and caches.",{"@type":2622,"name":2533,"acceptedAnswer":2630},{"@type":2624,"text":2631},"No. The header name is baked into every generated client, so changing X-API-Key silently strips credentials from requests already in production. Accept both headers server-side for a full deprecation window, publish the new one in the scheme description first, and only then flip the documented value.",{"@type":2622,"name":2542,"acceptedAnswer":2633},{"@type":2624,"text":2634},"Only if it is unasserted. Every mutation keyed on an operation ID or path string becomes a silent no-op the day someone renames the target, so pair each rule with a test that fails when it stops matching. Fifty tested lines are safe; three hundred untested ones will publish a wrong document eventually.",{"@type":2622,"name":2548,"acceptedAnswer":2636},{"@type":2624,"text":2637},"Yes, same repository, different published destination. Both artifacts come from one route table, so they cannot drift, and committing both means a diff shows you precisely when a route crossed from internal to public — which is exactly the review moment where an accidental exposure gets caught.",1784887028521]