← Back to OpenAPI Schema Generation for Spatial Types
Attach real, valid GeoJSON examples to a FastAPI request body so Swagger UI shows a usable Polygon instead of "string", and so generated clients ship with example payloads that actually validate.
Context & when to use
A correct schema tells a consumer what shape a geometry must have; a good example tells them what a real one looks like. Without an explicit example, FastAPI synthesises a placeholder from the schema — for a GeoJSON body that means coordinates: [0] or "string", which is not valid GeoJSON and cannot be posted from the “Try it out” panel without hand-editing. For a Polygon, whose coordinates is a triply-nested array of closed rings, no consumer reconstructs a valid value from the schema alone. Examples are what make the docs operable.
Reach for this whenever a route accepts a Feature, a raw geometry, or a FeatureCollection — which is most write endpoints in a spatial API. It builds directly on the models and discriminated union from OpenAPI schema generation for spatial types; here we focus purely on the example layer that sits on top of that schema. There are three insertion points — model-level json_schema_extra, parameter-level Body(..., examples=...), and the richer openapi_examples — and they compose rather than compete.
A word on why examples deserve their own treatment for geometry specifically. Most request bodies are flat objects a consumer can guess — a name, an email, an integer. GeoJSON is not: a Polygon’s coordinates is a three-level array ([[[lon, lat], ...]]) whose innermost ring must be closed, and the axis order is a convention no type system encodes. A consumer staring at the schema sees “array of array of array of number” and has no way to produce a valid value on the first try. A single, correct, copy-pasteable example removes that friction entirely and is often the difference between an API that gets adopted and one that generates support tickets.
Preconditions: FastAPI 0.106+ (for openapi_examples on Body), Pydantic 2.5+, and the geometry models already defined. Every example you write must validate against the model, or it becomes a lie that generators may propagate.
Data flow: where each example surfaces
Runnable implementation
One file, three example mechanisms layered so Swagger UI shows a selectable set of valid GeoJSON bodies. The models are the discriminated Feature/Geometry types from the parent guide.
# app/routes/geojson_body.py
from typing import Literal, Annotated, Any, Union
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
Position = Annotated[list[float], Field(min_length=2, max_length=3,
description="[longitude, latitude] (RFC 7946)")]
class Point(BaseModel):
type: Literal["Point"]
coordinates: Position
# (1) Model-level example: attached to the schema itself, so it appears
# wherever this model is referenced — responses included.
model_config = {
"json_schema_extra": {
"example": {"type": "Point", "coordinates": [-0.1278, 51.5074]}
}
}
class Polygon(BaseModel):
type: Literal["Polygon"]
# A ring must be closed: first position == last position, >= 4 positions.
coordinates: list[Annotated[list[Position], Field(min_length=4)]]
Geometry = Annotated[Union[Point, Polygon], Field(discriminator="type")]
class Feature(BaseModel):
type: Literal["Feature"]
geometry: Geometry
properties: dict[str, Any] | None = None
app = FastAPI()
@app.post("/zones")
async def create_zone(
feature: Feature = Body(
...,
# (2) openapi_examples: multiple *named* request examples. Each renders
# as an entry in the Swagger "Examples" dropdown. Unlike the older
# `examples=[...]` list, these carry a summary and description.
openapi_examples={
"point_poi": {
"summary": "Point of interest",
"description": "A single Point in EPSG:4326, lon before lat.",
"value": {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-0.1278, 51.5074]},
"properties": {"name": "Trafalgar Square", "kind": "poi"},
},
},
"polygon_zone": {
"summary": "Closed Polygon catchment",
"description": "Note the ring is closed — last == first position.",
"value": {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-0.130, 51.500], [-0.120, 51.500],
[-0.120, 51.510], [-0.130, 51.510],
[-0.130, 51.500],
]],
},
"properties": {"zone": "central", "kind": "catchment"},
},
},
"invalid_axis_order": {
"summary": "WRONG — lat/lon swapped (for contrast)",
"description": "Do not copy: coordinates are [lat, lon]; London lands off Somalia.",
"value": {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [51.5074, -0.1278]},
"properties": {},
},
},
},
),
):
"""Accept a GeoJSON Feature. The named examples above populate the
'Try it out' body; the point_poi and polygon_zone examples validate,
the invalid_axis_order one is documented deliberately as a counter-example."""
return {"received_type": feature.geometry.type, "properties": feature.properties}The three counterpart mechanisms, ranked by scope: json_schema_extra on the model is broadest (follows the schema everywhere), openapi_examples is per-endpoint and richest (named, described, multiple), and the older Body(..., examples=[...]) list is a lighter middle ground when you only need one or two unnamed samples and do not need summaries.
Documentation lands in two very different places, and only some of it survives the trip to a generated client.
Key parameters & options
| Mechanism | Where it lives | Multiple? | Named + described | Best for |
|---|---|---|---|---|
model_config["json_schema_extra"]["example"] | On the Pydantic model | One canonical | No | A default example reused across every route using the model |
Body(..., examples=[{...}]) | Endpoint parameter | Yes (list) | No | A couple of quick request samples, no metadata |
Body(..., openapi_examples={...}) | Endpoint parameter | Yes (dict) | Yes (summary, description) | Public docs: labelled valid + counter-examples |
Field(..., examples=[...]) | Individual model field | Yes | No | Illustrating one member (e.g. a properties shape) |
Notes that matter for geometry specifically:
openapi_exampleskeys become the dropdown labels — make them human-readable (polygon_zone, notex2).- A
valueinopenapi_examplesis emitted verbatim intorequestBody.content."application/json".examples; it is not re-validated by FastAPI at schema-build time, which is exactly why an invalid example can slip through. json_schema_extraaccepts either a dict or a callable; use the callable form if you need to strip internal fields before publishing.
Inline schemas feel convenient at the point of writing and are paid for by every consumer.
Gotchas & failure modes
- Example does not validate against its own schema. FastAPI does not check
openapi_examplesvalues, so aPolygonexample with an unclosed ring or aPointwith a single-elementcoordinatesrenders happily in/docsbut 422s the moment someone clicks “Execute”. Assert examples in a test (see Verification) so drift fails CI. - Coordinate order silently wrong.
[lat, lon]is structurally valid GeoJSON — two floats in an array — so nothing rejects it, but the geometry lands in the wrong hemisphere. Theinvalid_axis_ordercounter-example above exists to teach this; keep every real example in[lon, lat]per RFC 7946, matching the enforcement in validating WKT and GeoJSON with Pydantic v2. - Nested Feature example collapses to the geometry only. If you attach the example to the
Geometryunion but the body type isFeature, Swagger shows a bare geometry, not a full Feature. Attach the example at the level that matches the body parameter’s type. example(singular, 3.0) vsexamples(3.1) confusion. On OpenAPI 3.1,openapi_examplesmaps to theexamplesobject; a stray top-levelexamplekey you added by hand may be dropped. Let FastAPI emit the examples rather than hand-editing the document.- Giant Polygon example bloats
/openapi.json. A 5,000-vertex example makes the schema payload megabytes and slows/docsload. Keep example geometries small (a 4–6 vertex ring is plenty); real payloads can be large, documentation examples should not be. - Response examples are separate from request examples. A
json_schema_extraexample on a model flows into both the request schema and any response that uses the model, butopenapi_examplesonBodyis request-only. If you want a documented response, attach the example to the model (or useresponses=on the route decorator); a request-side example never appears underresponses. FeatureCollectionbodies need a whole-collection example, not a per-feature one. Attaching an example toFeaturedocuments a single feature; a route that accepts aFeatureCollectionstill shows a bare{}for the collection wrapper unless you attach a full{"type":"FeatureCollection","features":[...]}example at the body level. Document at the exact type the endpoint receives.
Verification
Confirm the example is present in the document and that it actually round-trips:
# 1. The named examples are in the request body
curl -s http://localhost:8000/openapi.json \
| jq '.paths."/zones".post.requestBody.content."application/json".examples | keys'
# -> ["invalid_axis_order", "point_poi", "polygon_zone"]
# 2. The valid Polygon example round-trips (should return 200)
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8000/zones \
-H "Content-Type: application/json" \
-d '{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-0.130,51.500],[-0.120,51.500],[-0.120,51.510],[-0.130,51.510],[-0.130,51.500]]]},"properties":{"zone":"central"}}'
# -> 200Guard every valid example against its schema in a test so a bad edit cannot ship:
# tests/test_examples_validate.py
from app.routes.geojson_body import Feature, app
def test_named_examples_validate():
route = next(r for r in app.routes if getattr(r, "path", "") == "/zones")
body_field = route.dependant.body_params[0].field_info
for name, ex in body_field.openapi_examples.items():
if name.startswith("invalid_"):
continue # counter-examples are meant to fail
Feature.model_validate(ex["value"]) # raises if the example is brokenRelated
- OpenAPI Schema Generation for Spatial Types — the discriminated-union models these examples decorate
- Generating Typed Clients from Spatial OpenAPI Schemas — where documented examples end up in the generated SDK
- Strict Pydantic Validation for Geometry — make sure the examples you document are the ones the validator accepts