Examples in the OpenAPI Schema with FastAPI
Key takeaways:
Field(examples=[...])documents one property and travels with the model everywhere.json_schema_extraon the model config carries a whole sample payload.Body(openapi_examples={...})gives one operation a named, described set of examples.- Route-level examples win the request-body dropdown; the model example still renders elsewhere.
- Examples are never validated against the model, so a stale one produces confidently wrong docs.
This guide extends JSON Schema Customization, which covers shaping the generated schema; this page is about the part of that schema a human actually reads first.
The Problem This Solves
A consumer opens your /docs page and sees a request body sample of {"customer_email": "string", "currency": "string", "items": []}. It is technically accurate and completely useless. They cannot tell whether currency wants GBP or British Pound, whether items may be empty, or what an SKU looks like. So they guess, get a 422, and open a support ticket — or worse, copy the placeholder into an integration test and ship it.
Three different mechanisms in FastAPI fix this, they surface in different places, and the one people reach for first is usually not the one they wanted.
Why It Happens
The three mechanisms live at different levels of the OpenAPI document, and that is the entire explanation for their different behaviour.
Field(examples=[...]) is Pydantic's. It writes an examples array into the property's own schema object, inside components.schemas.<Model>.properties.<field>. Because it lives on the model, it appears everywhere that model is referenced — request bodies, response bodies, nested inside other models — and it survives being reused across a dozen endpoints.
json_schema_extra is also Pydantic's, one level up. Whatever dictionary you supply is merged into the model's schema object verbatim. Putting an examples key there attaches a full sample payload to the model itself. It is the escape hatch for anything JSON Schema supports that Pydantic has no dedicated argument for.
Body(openapi_examples={...}) is FastAPI's, and it lives outside the schema entirely. It writes into paths.<path>.<method>.requestBody.content["application/json"].examples — the OpenAPI media-type object, not the JSON Schema. That location is why it supports things JSON Schema cannot express: each entry carries a summary and a description alongside its value, which is what Swagger UI renders as a labelled dropdown.
Because the media-type object is more specific than the schema it references, a route-level openapi_examples mapping wins the request-body display for that operation. The model-level example is not discarded — it is still in components and still renders where the model is shown for other purposes, including the response sample.
None of the three is ever validated. Pydantic treats an example as opaque metadata and copies it into the schema without checking it against the field it decorates. That is a deliberate design choice — examples sometimes need to be deliberately invalid — and it is the reason examples rot.
The Fix
Use all three, at the level each is good at. Field examples for values whose format is not obvious, a model example for the canonical payload, and route examples where a specific operation benefits from several named cases.
"""Attach examples at field, model and route level and read them back out of the OpenAPI document."""
from typing import Annotated
from fastapi import Body, FastAPI
from pydantic import BaseModel, ConfigDict, Field
class LineItem(BaseModel):
sku: Annotated[str, Field(examples=["SKU-4417"], description="Catalogue identifier.")]
quantity: Annotated[int, Field(gt=0, examples=[2], description="Units ordered.")]
class CreateOrder(BaseModel):
"""A model-level example is the whole payload, ready to copy out of Swagger UI."""
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"customer_email": "ada@example.com",
"currency": "GBP",
"items": [{"sku": "SKU-4417", "quantity": 2}],
}
]
}
)
customer_email: Annotated[str, Field(examples=["ada@example.com"])]
currency: Annotated[str, Field(min_length=3, max_length=3, examples=["GBP", "USD"])]
items: list[LineItem]
The route adds two named cases, one of which is deliberately invalid so a reader can watch the 422 happen from inside the docs page:
@app.post("/orders", response_model=Order, operation_id="createOrder", tags=["orders"])
async def create_order(
order: Annotated[
CreateOrder,
Body(
openapi_examples={
"single_item": {
"summary": "One line item",
"description": "The common case.",
"value": {
"customer_email": "ada@example.com",
"currency": "GBP",
"items": [{"sku": "SKU-4417", "quantity": 1}],
},
},
"rejected": {
"summary": "Invalid — currency too long",
"description": "Swagger UI will submit this and show you the 422.",
"value": {
"customer_email": "ada@example.com",
"currency": "POUNDS",
"items": [{"sku": "SKU-4417", "quantity": 1}],
},
},
}
),
],
) -> Order:
return Order(id=9001, **order.model_dump())
Reading all three back out of the generated document, from a real run of this app:
$ GET /_meta/examples
200 OK
{
"route_level_openapi_examples": [
"rejected",
"single_item"
],
"model_level_example": [
{
"currency": "GBP",
"customer_email": "ada@example.com",
"items": [
{
"quantity": 2,
"sku": "SKU-4417"
}
]
}
],
"field_level_examples": {
"customer_email": [
"ada@example.com"
],
"currency": [
"GBP",
"USD"
],
"items": null
},
"nested_field_examples": {
"sku": [
"SKU-4417"
],
"quantity": [
2
]
}
}
Every layer landed where the previous section said it would. The route examples are in the media-type object under their names. The model example sits on CreateOrder itself. The field examples are on the properties, and items is null because no example was declared for it — the nested LineItem supplies its own instead, which is the reuse benefit of putting examples on models rather than on endpoints.
And the deliberately invalid example does produce the 422 it promises:
$ POST /orders {"customer_email": "ada@example.com", "currency": "POUNDS", "items": [{"sku": "SKU-4417", "quantity": 1}]}
422 Unprocessable Entity
{
"detail": [
{
"type": "string_too_long",
"loc": [
"body",
"currency"
],
"msg": "String should have at most 3 characters",
"input": "POUNDS",
"ctx": {
"max_length": 3
}
}
]
}
What this actually looks like
This is the real /docs page for the app above, photographed from a running instance with the operation expanded:

The layering is visible in one screen. The Examples dropdown above the request body is the route-level openapi_examples, labelled with the summary you wrote and captioned underneath with the description — the second entry, "Invalid — currency too long", is one selection away and submits through Try it out to produce the 422 above.
Below, the 200 response's Example Value shows "quantity": 2 and the model-level payload, because responses have no route-level examples to override them. That is the precedence rule made concrete: the route examples took the request body, the model example kept everything else.
Verification
Examples are not validated, so validate them yourself. This is a five-line test that keeps every published example honest:
def test_every_openapi_example_is_valid():
spec = app.openapi()
body = spec["paths"]["/orders"]["post"]["requestBody"]["content"]["application/json"]
for name, example in body["examples"].items():
if name == "rejected":
continue # deliberately invalid, documented as such
CreateOrder.model_validate(example["value"])
def test_model_example_is_valid():
for example in CreateOrder.model_json_schema()["examples"]:
CreateOrder.model_validate(example)
Run it in CI and a field rename that leaves the examples behind fails the build instead of shipping. This is the single highest-value test on this page, because the failure mode it catches — documentation that is wrong with total confidence — is otherwise only discovered by a frustrated consumer.
It is also worth asserting that fields consumers commonly get wrong have examples at all:
def test_ambiguous_fields_are_exemplified():
props = CreateOrder.model_json_schema()["properties"]
for field in ("currency", "customer_email"):
assert props[field].get("examples"), f"{field} ships with no example"
Trade-offs and When Not To
Examples are duplication, and duplication drifts. Every example is a copy of a payload shape that also exists in the model. The CI test above is what makes that duplication safe; without it, examples are a liability that grows with the API.
Do not put real data in examples. They are public, they end up in generated SDK docstrings and in search results. Real customer emails, real account ids and anything resembling a live token do not belong there.
Route-level examples do not scale across an API. They live on one operation and cannot be reused, so an API with forty endpoints each carrying three named examples becomes a maintenance problem. Reach for them where the multi-case story genuinely helps — a polymorphic body, a deliberately-failing case, a rarely-used optional shape — and rely on model examples everywhere else. Discriminated bodies in particular deserve one named example per variant, as Discriminated Unions in OpenAPI shows.
The older example singular is deprecated. OpenAPI 3.1 and JSON Schema use the plural examples array. FastAPI still accepts a singular example argument for compatibility, but new code should use examples=[...] — the plural is what modern tooling reads.
Examples do not replace descriptions. An example shows the shape; it does not explain the semantics. "idempotency_key": "req-abc-123" tells a reader nothing about how long the key is retained. Pair every non-obvious example with a description.
FAQ
What is the difference between Field(examples=...) and openapi_examples?Field(examples=[...]) attaches a list of sample values to one property inside the JSON Schema, so it travels with the model everywhere the model is referenced. Body(openapi_examples={...}) attaches named, described examples to one request body of one operation, and it is what fills the Examples dropdown in Swagger UI.
Why does my model-level example not appear in the request body dropdown?
Because a route-level openapi_examples mapping takes precedence for that operation's request body. The model example is still in the document and still renders wherever the model is shown, including the response sample, but the dropdown shows the named route examples instead.
Do examples affect validation? No. Examples are documentation metadata only — Pydantic never validates an example against the model, and an example that would fail validation still ships. That is worth knowing, because it means a stale example produces confidently wrong docs rather than an error.
Where should I put examples so they stay accurate? On the model, next to the fields and constraints they illustrate, so that anyone changing the field sees the example in the same diff. Examples written in separate documentation drift within a release or two.
Can I have several named examples for one endpoint?
Yes, that is exactly what Body(openapi_examples={...}) is for. Each entry has a summary, an optional description and a value, and Swagger UI renders them as a dropdown so a reader can load a realistic payload — including one that deliberately fails — into Try it out.
Related Reading
- Up to the topic: JSON Schema Customization.
- Broader control over the generated document: Customizing OpenAPI Schema Generation in FastAPI.
- One named example per variant of a tagged body: Discriminated Unions in OpenAPI.
- Grouping the documented operations so the examples are findable: Router Tags and OpenAPI Grouping.
- The constraints your examples illustrate: Custom Validators and Field Constraints.