← Back to OpenAPI Schema Generation for Spatial Types
Compile a spatial openapi.json into typed Python and TypeScript SDKs, and map GeoJSON discriminated unions onto client types so map frontends narrow geometry by its type field instead of casting any.
Context & when to use
Once the API emits an accurate schema — typed geometry models, a oneOf with a discriminator, valid examples — the payoff is that clients no longer have to be written by hand. A generator reads openapi.json and produces request/response models, method stubs, and (crucially for geospatial work) a tagged union for geometry that a TypeScript map frontend can switch on. Generate clients when more than one team or language consumes the API, when you want compile-time safety against schema drift, or when a map UI needs geometry types it can narrow rather than probe at runtime.
This is the downstream end of the pipeline in OpenAPI schema generation for spatial types: the quality of the generated client is capped by the quality of the schema, so a clean discriminated union upstream is what makes a clean sum type downstream. Prefer generated clients over hand-written HTTP calls for anything beyond a throwaway script; the main cost is a regeneration step in CI whenever the schema changes.
The two generators covered here occupy different niches. openapi-python-client is Python-native, produces attrs-based models and an httpx transport, and tends to render oneOf unions faithfully as typing.Union with per-variant classes — a good fit when the consumer is another Python service. openapi-generator-cli is the polyglot workhorse (dozens of target languages) and is usually how a TypeScript map frontend gets its client; its typescript-fetch generator maps a discriminated oneOf onto a TypeScript tagged union that narrows on the discriminator. Pick per consumer; there is no need to standardise on one generator across languages.
Preconditions: a reachable openapi.json (from a running app or exported to a file), Python 3.10+ with pipx for openapi-python-client, and Node.js with npx (plus a JRE) for openapi-generator-cli.
Generator pipeline
Runnable implementation
Export the schema, generate both clients, then narrow the geometry union in each language.
# 1. Export openapi.json from the running FastAPI app (or curl it directly)
curl -s http://localhost:8000/openapi.json -o openapi.json
# 2. Typed PYTHON client (attrs models + httpx, fully typed)
pipx run openapi-python-client generate \
--path openapi.json \
--meta pyproject \
--output-path ./spatial_client_py
# Produces: spatial_client_py/spatial_features_api_client/models/{point,polygon,feature}.py
# with a `Geometry` union and per-variant classes keyed on `type`.
# 3. Typed TYPESCRIPT client (fetch-based, tagged unions preserved)
npx @openapitools/openapi-generator-cli generate \
-i openapi.json \
-g typescript-fetch \
-o ./spatial_client_ts \
--additional-properties=supportsES6=true,withInterfaces=true,useSingleRequestParameter=true
# Produces: spatial_client_ts/models/{Point,Polygon,Feature}.ts
# and a Geometry.ts discriminated union type.Using the generated Python client — the discriminated geometry becomes a Union, narrowed with isinstance:
# consume_py.py
from spatial_features_api_client import Client
from spatial_features_api_client.models import Feature, Point, Polygon
from spatial_features_api_client.api.default import create_feature_features_post
client = Client(base_url="http://localhost:8000")
# Build a typed request body — mypy checks the geometry variant.
body = Feature.from_dict({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-0.1278, 51.5074]},
"properties": {"name": "Trafalgar Square"},
})
resp: Feature = create_feature_features_post.sync(client=client, body=body)
# Narrow the union on the concrete class the generator produced.
geom = resp.geometry
if isinstance(geom, Point):
lon, lat = geom.coordinates
print(f"point at {lon}, {lat}")
elif isinstance(geom, Polygon):
print(f"polygon with {len(geom.coordinates[0])} vertices in outer ring")Using the generated TypeScript client in a map frontend — switch on the discriminator, no any:
// consume.ts
import { DefaultApi, Configuration, Feature, Geometry } from "./spatial_client_ts";
const api = new DefaultApi(new Configuration({ basePath: "http://localhost:8000" }));
const feature: Feature = await api.createFeatureFeaturesPost({
feature: {
type: "Feature",
geometry: { type: "Point", coordinates: [-0.1278, 51.5074] },
properties: { name: "Trafalgar Square" },
},
});
// The generator maps oneOf+discriminator to a tagged union: narrow on `type`.
function toLngLat(geom: Geometry): [number, number] {
switch (geom.type) {
case "Point":
return [geom.coordinates[0], geom.coordinates[1]]; // [lon, lat]
case "Polygon":
return geom.coordinates[0][0] as [number, number]; // first vertex
default:
// Exhaustiveness check — a new geometry type breaks the build here.
throw new Error(`unhandled geometry: ${(geom as { type: string }).type}`);
}
}Key parameters & options
| Generator | Flag | Effect |
|---|---|---|
openapi-python-client | --meta pyproject | Emit an installable package with pyproject.toml rather than bare modules |
openapi-python-client | --config config.yml | Override class names, add field aliases, post-process hooks |
openapi-generator-cli | -g typescript-fetch | Fetch-based TS client; typescript-axios if you prefer axios |
openapi-generator-cli | withInterfaces=true | Emit interfaces alongside classes so unions stay structural |
openapi-generator-cli | useSingleRequestParameter=true | One request-object arg per method — friendlier for many params |
openapi-generator-cli | --type-mappings | Remap a schema type to a client type (rarely needed for GeoJSON) |
| both | pin generator version | Reproducible output; unpinned generators drift model names across CI runs |
For the TypeScript generator, withInterfaces=true plus a discriminated oneOf is what yields a real narrowable union; without the discriminator upstream the generator falls back to an object and the switch above will not type-check.
openapi-python-client accepts a --config YAML that is worth setting up for spatial APIs: it lets you pin generated class names (so Point does not become PointType1 when the generator disambiguates), add field-level property_overrides, and register post-generation hooks. A minimal config that stabilises geometry class names looks like:
# config.yml — passed via --config
class_overrides:
Point:
class_name: GeoPoint
module_name: geo_point
use_path_prefixes_for_title_model_names: falseRegeneration is meant to be cheap and frequent, so keep both the export command and the generate command in a single make client target and run it in CI on every schema change.
Gotchas & failure modes
oneOfwithout a discriminator generates an untagged union. If the schema hasoneOfbut nodiscriminator.mapping,openapi-generator-cliemitsPoint | Polygonwith no tag, soswitch (geom.type)does not narrow and you are back to runtime casts. Fix upstream: addField(discriminator="type")as shown in OpenAPI schema generation for spatial types.additionalProperties: trueon geometry weakens the type. If a geometry model allows extra properties, some generators widen it to[key: string]: any, defeating strictness. Setmodel_config = {"extra": "forbid"}on the geometry models so the schema emitsadditionalProperties: false.- Schema drift silently breaks the client. Regenerating against a changed schema can rename or drop model classes; consumers fail to compile with no warning until then. Regenerate in CI and fail the build on a non-empty
git diffof the generated directory so drift is caught at the source, in step with versioning geospatial APIs without breaking clients. - Unbounded coordinate arrays generate
number[]with no shape. If the schema did not constraincoordinateslength, the client types a position asnumber[], soconst [lon, lat]gives no safety. Bound the position array upstream sominItems/maxItemsreach the generator. - JRE missing for
openapi-generator-cli. The CLI is a Java tool;npx @openapitools/openapi-generator-clifails withUnable to locate a Java Runtimeif no JDK/JRE is onPATH. Install a JRE 11+ or use the Docker imageopenapitools/openapi-generator-cli. - FastAPI operationIds produce ugly method names. Names like
create_feature_features_postcome from the default operationId. Set explicitoperation_id=on routes (or a customgenerate_unique_id_function) before exporting the schema for cleaner generated method names.
Verification
Confirm the generated client compiles and actually round-trips against the live API:
# Python client: install locally and type-check
pip install -e ./spatial_client_py
python -m mypy consume_py.py # 0 errors => geometry union narrows cleanly
python consume_py.py # prints "point at -0.1278, 51.5074"
# TypeScript client: type-check the union narrowing
cd spatial_client_ts && npm install && npx tsc --noEmit ../consume.ts
# A missing `case` for a geometry type fails the exhaustiveness check at build time.A successful mypy/tsc pass proves the discriminated union survived the schema→client trip; a live python consume_py.py returning the echoed feature proves the wire contract matches the generated models.
Related
- OpenAPI Schema Generation for Spatial Types — produce the discriminated-union schema these generators consume
- Documenting GeoJSON Request Bodies in OpenAPI — the request examples that ship into the generated SDK
- API Versioning for GIS Endpoints — manage schema drift so regenerated clients do not break consumers