OpenAPI and the Docs

The document your annotations generate, and how much of an API's usability is decided by it.

Overview

What is generated

OpenAPI is a standard way to describe an HTTP API in a machine-readable file. It lists every path, every method, every parameter, and every response. FastAPI builds that file for you automatically, from your annotations, and then uses it to serve interactive documentation pages at /docs and /redoc.

You get this for free, and it is worth taking seriously. Documentation is often the first thing another developer sees of your API. If it is clear, they can try calls straight from the browser. If it is vague, they will ask you questions or give up. The same file can also generate client libraries and feed testing tools.

This article starts with what is generated and where you can look at it. It then explains the header of the document, with the title, version, and description that identify your API. After that it covers tags, which group endpoints into sections so a large API stays readable.

The highest-value addition comes next: examples. A realistic sample request and response teaches more than a paragraph of explanation. The article shows how to add them to your models and endpoints. It closes with documenting failures, so the docs list the error responses a client should expect and not only the success case.

A few small additions here can turn passable documentation into something people enjoy using.

The documentation pages are also a testing tool. You can fill in parameters and send real requests from the browser, which makes them a fast way to check your own work while you build. Each section is short and ends with a habit you can apply straight away. You finish knowing how to turn the generated pages into real documentation that answers questions before anyone has to ask them, and that stays accurate as your code changes.

app.openapi() returns one OpenAPI doc showing each route: paths, methods, params, request bodies, response shapes, status codes, descriptions and examples.

It's assembled from things you already wrote. The path in the decorator, the params in the signature, the models, the constraints, the docstrings. There's no split specification to maintain and no way for it to drift. It *is* the code.

FastAPI serves it at /openapi.json, renders it at /docs (Swagger UI) and /redoc (ReDoc).

Worth knowing

app.openapi() assembles the document from your models and signatures. The docs page at /docs renders it.
title, version and description on FastAPI() become the document's header; a function's docstring becomes an endpoint's description.
Tags group endpoints into sections, and openapi_tags gives each section a description.
examples pre-fill the interactive request form — the difference between a caller's first attempt working and being a guess.
responses= documents the failure shapes, which is what lets a generated client type its errors.
include_in_schema=False hides an endpoint from the document without protecting it. Undocumented is not private.

OpenAPI and the Docs

The doc your annotations make. How much of an API's usability is decided by it.

The document, and where it comes from

app.openapi() assembles one OpenAPI document from every model and signature. Nothing else was written to produce it.

example_01.pyFastAPI
Output

Tags organise it

Tags group endpoints into sections, and metadata on the app gives each section a description.

example_02.pyFastAPI
Output

Examples fill the Try it out form

The single highest-value thing you can add: a caller gets a working request instead of an empty box.

example_03.pyFastAPI
Output

Documenting the failures

responses= puts the error shapes in the document, so a generated client can type them.

example_04.pyFastAPI
Output

Hiding what should not be published

include_in_schema=False keeps an internal endpoint out of the document while leaving it working.

example_05.pyFastAPI
Output

Reading it as a review

One line, and you see what a consumer sees. It is the fastest review available for an API model.

example_06.pyFastAPI
Output

The most hands-on use of this module. One line:

print(json.dumps(app.openapi(), indent=2))

and you see just what your consumers see.

What it well surfaces: fields whose names aren't self-explanatory and have no description. A str param where a Literal would have given clients a set of options. A rule enforced by a checker that appears nowhere. A needed field you meant to default. An endpoint with no example.

The last editor above automates two of those checks. It's worth running over a real model. The results are often uncomfortable and always cheap to fix.

The header

FastAPI(title=..., version=..., description=...) turns the top of the doc and the top of the docs page.

The description supports Markdown and is the one piece of genuine prose in the whole thing. It's worth writing. What the API is for, how authentication works, what the rate limits are. Here To get a key. That is the first thing a new consumer reads. The option is that they read nothing.

version should be your API's version, not your library's. Consumers pin against it.

Tags

Tags group endpoints into sections. On a router, tags=["modules"] applies to each route in it.

openapi_tags on the app gives each tag a description. This Turns a paragraph above that section in the docs.

For an API past a dozen routes this is the gap between a navigable doc and a flat alphabetical list. It costs one arg per router.

Examples are the highest-value addition

all else in this module is worth doing. This is the one that changes whether people succeed.

A Field(examples=[...]) or a model-level json_schema_extra={"examples": [...]} pre-fills the interactive form. A developer opens /docs, presses Try it out. Gets a request that works. Instead of an empty box they've to guess at, with a 422 for their first three attempts.

That gap is measurable in how many integrations get finished.

Write examples that are realistic rather than minimal. "title": "string" is what the gen makes without you. "title": "Dot Product" shows what the field is really for.

Documenting failures

response_model covers the success case. all else is undocumented unless you say so:

responses={404: {"model": Problem, "description": "No such module"}}

Worth doing for the failures a caller is expected to handle. A made client can then type its errors. A human reading the docs knows what a rejection looks like before causing one.

Hiding endpoints

include_in_schema=False keeps a route out of the doc. handy for inner endpoints, legacy paths kept for one client. Health checks that would only clutter the page.

One warning worth being clear about: this isn't access control. The endpoint still works, still accepts requests. Is just as reachable as before. It's invisible, not protected. If it shouldn't be called, it needs a dependency, not a flag.

deprecated=True is the other half of retiring a thing. The endpoint keeps working and the docs show it as deprecated. This Gives consumers a signal without breaking them.

What it feeds

The doc is read by more than the docs page. This Is why its quality compounds.

Client gens give typed clients in a dozen languages from it. A vague schema makes a vague client. Each consumer then writes their own guesses.

Contract tests can assert that a change didn't break the published shape.

API gateways can validate requests before they reach you.

LLM tooling increasingly reads schemas to decide how to call an API.

None of those read your source. All of them read this.

The limits

Some things can't be expressed, and pretending else misleads consumers.

checker logic, cross-field rules, anything needing a lookup. None has a JSON Schema equal. Where a rule matters and can't be declared, put it in the docstring or the model's description. At least a human reading the docs learns about it.

An endpoint whose real constraints live in code the schema can't see is an endpoint whose docs is quietly incomplete.

Mistakes people make with OpenAPI and the Docs

**Treating include_in_schema=False as security.** The endpoint still works and is just as reachable. Invisible isn't protected.

Leaving fields undescribed. weight needs a description - of what, in what unit. A name that isn't self-explanatory and has no description makes docs that strictly exists.

No examples. The made placeholder is "string". A caller's first three attempts then return 422. Some of them stop there.

Documenting only success. An endpoint that can 404 should say so, or a made client has no type for failure.

**Using str where a Literal belongs.** The schema then offers no options. No client can render a choice and no consumer knows what is valid.

Never looking at it. It's one line. It's the only view of your model that matches what consumers receive.

Versioning it with the library version. Consumers pin against your API's version, not your package's.

The review worth doing

Print the doc for a model you've just written and read it as a stranger.

What it surfaces: needed fields you meant to default, patterns that should have been enumerations, rules that live in checkers and appear nowhere, endpoints with no summary. Fields whose names carry meaning only to whoever wrote them.

each one of those is cheap to fix at that moment and costly once clients exist. By then the shape is a contract.

Where to go after OpenAPI and the Docs

Putting the pieces together: how a FastAPI project is laid out once it's more than one file.

What it costs to skip

An API without a good doc still works. The cost is paid by all else.

each consumer writes their own guesses about shapes. each made client is untyped. each question that could have been answered by reading turns a message to whoever wrote it. And each change is maybe breaking. No one wrote down what the contract was.

The work to avoid that is small and front-loaded. A description on the app, tags on the routers, descriptions on the fields whose names aren't obvious, one example per body model. responses= on the failures a caller is expected to handle.

An hour, once, and it's read by each person and tool that touches the API later.

The chain it feeds

Worth holding in mind, because it shows why small omissions matter.

Your models make schemas. FastAPI assembles them into one doc. That doc is read by the interactive docs, by client gens in a few languages, by API gateways, by contract-testing tools. Increasingly by LLM tooling deciding how to call you.

A missing description is missing in all of them. So is a str that should have been a Literal. None of those tools read your source code. None of them can ask.

OpenAPI and the Docs: summary

The doc is made from your models, signatures, decorators and docstrings. Served at /openapi.json, /docs and /redoc.

Give the app a title, version and description. Group routes with tags and show the groups with openapi_tags. Doc failures with responses=. And write examples. This Are the single highest-value addition because they turn a caller's first attempt from a guess into a working request.

include_in_schema=False hides without protecting. And the fastest review on hand for any API model is printing the doc and reading it as a stranger would.

The limits, stated plainly

Some things can't be expressed, and knowing which keeps the docs honest.

checker logic has no schema equal. A rule enforced by a field_validator is invisible to each consumer.

Cross-field rules likewise. "End date must be after start date" appears nowhere.

Anything needing a lookup - does this link exist, is this name taken - isn't expressible and shouldn't be attempted.

Custom output changes output without changing the schema unless return_type is set. This Is how a doc quietly starts showing a thing the endpoint no longer returns.

Where a rule matters and can't be declared, put it in the model's docstring or the field's description. It won't be machine-readable. A human reading the docs will at least learn it exists rather than discovering it through a rejection.

Who reads it

Worth being concrete, because the audience is larger than the docs page.

A developer integrating with you opens /docs, presses Try it out. Either gets a working request or doesn't. That first minute decides how the rest goes.

A client gen turns the doc into a typed library. Its quality is fully your schema's quality.

A gateway may validate requests against it before they reach your process.

A contract test can assert that today's doc is matching with yesterday's.

An LLM increasingly reads it to decide how to call you. Reads only what is written.

None of them can ask a question. None of them read your source. all they know is in the doc. This Is why the descriptions and examples aren't decoration.

A closing thought

The made doc is the closest thing an API has to a public interface meaning. It's made fully as a side effect of writing types.

That is unusual and worth appreciating. In most stacks the specification is a split artefact that somebody keeps. It drifts from the setup at once because nothing forces them together.

Here it can't drift, because there's only one source. What varies is how much you put into that source - and the gap between a doc consumers can build against and one they've to guess at is a handful of descriptions, one example per model. A Literal where a str would have done.

Questions people ask about OpenAPI and the Docs

Where is the OpenAPI schema? At /openapi.json by default. Swagger UI is at /docs and ReDoc is at /redoc.

How do I turn the docs off? Pass docs_url=None, redoc_url=None, and openapi_url=None to FastAPI().

How do I group endpoints? Add tags=["name"] to routes or routers, and describe tags with openapi_tags.

How do I add examples? Use Field(examples=[...]) on model fields, or model_config json_schema_extra for a full example.

Can I generate a client from the schema? Yes. Tools such as openapi-generator consume openapi.json and produce typed clients in many languages.

How do I hide an endpoint from the docs? Pass include_in_schema=False to the decorator.

OpenAPI and the Docs in one line

Your annotations already wrote your API docs. The only question is how much you put into them. The answer is decided by a handful of descriptions, one example per body model. A Literal wherever a str would have left a consumer guessing.

And the cheapest habit on hand is to print the doc once for each model you write. Five minutes, no tooling. It shows you the API as a stranger receives it rather than as its author remembers it.

One more habit worth the minute it costs. After adding an endpoint, open /docs and try it as a stranger would. If your own first attempt returns a 422. Will everybody else's. You're the only person who can still fix it cheaply.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Where does the OpenAPI document come from?

  2. What does `include_in_schema=False` do?

  3. Which addition most improves a first-time caller's experience?

  4. A rule lives in a `field_validator`. What does the schema say about it?

Cheat sheet

OpenAPI and the Docs

Open the auto-generated Swagger UI, trace every field back to your type annotations, and see how much of an API's usability the schema decides.

FASTAPI · vizlearn.in/fastapi/openapi_and_docs.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.