Overture Schema Codegen

August 26, 2026 ยท View on GitHub

Generates documentation from Overture Maps Pydantic schema definitions.

Pydantic's model_json_schema() flattens the schema's domain vocabulary into JSON Schema primitives. NewType names disappear, constraint provenance is lost (which NewType contributed which bound), custom constraint classes lose their identity (a GeometryTypeConstraint becomes an anonymous enum array), and discriminated union structure collapses into anyOf arrays with duplicated fields.

Navigating Python's type annotation machinery -- NewType chains, nested Annotated wrappers, union filtering, generic resolution -- is complex. The codegen does it once. analyze_type() unwraps an annotation into a FieldShape, a tree-shaped target-independent representation. Extractors build specs from FieldShape. Renderers consume specs without touching the type system. New output targets (Arrow schemas, PySpark expressions) add renderers, not extraction logic.

Usage

# Generate markdown documentation for all themes
overture-codegen generate --format markdown --output-dir docs/schema/reference

# Generate for a single theme
overture-codegen generate --format markdown --tag overture:theme=buildings --output-dir out/

# List discovered models
overture-codegen list

The generator discovers models via overture.models entry points (provided by theme packages like overture-schema-theme-buildings), extracts type information, and renders output pages with cross-page links, constraint descriptions, and validated examples.

Architecture

Four layers with strict downward imports -- no layer references the one above it:

Rendering            Output formatting, all presentation decisions
    ^
Output Layout        What to generate, where it goes, how outputs link
    ^
Extraction           FieldShape, FieldSpec, RecordSpec, UnionSpec
    ^
Discovery            discover_models() from overture-schema-system

Discovery loads registered Pydantic models via entry points. The return dict includes both concrete BaseModel subclasses (like Building) and discriminated union type aliases (like Segment). Both satisfy the ModelSpec protocol and flow through the same pipeline.

Extraction unwraps type annotations into specs. analyze_type() is the central function -- a single iterative loop that peels NewType, Annotated, Union, and container wrappers, accumulating constraints tagged with the NewType that contributed them. Domain-specific extractors (model_extraction, union_extraction, enum_extraction, newtype_extraction, numeric_extraction) call analyze_type() for field types and produce spec dataclasses.

Output Layout determines what artifacts to generate and where they go. Supplementary type collection walks expanded feature trees to find referenced enums, NewTypes, and sub-models. Path assignment maps every type to an output file path mirroring the Python module structure. Link computation and reverse references enable cross-page navigation.

Rendering consumes specs and owns all presentation decisions. Markdown output uses Jinja2 templates for feature pages (with field tables, constraint sections, and examples), enum pages, NewType pages, and aggregate numeric/geometry reference pages.

markdown/pipeline.py orchestrates the full pipeline without I/O, returning list[RenderedPage]. The CLI writes files to disk with Docusaurus frontmatter.

Programmatic use

analyze_type() returns a 3-tuple: the annotation's FieldShape, whether the field accepts None, and the first Field(description=...) encountered while unwrapping.

from overture.schema.buildings import Building
from overture.schema.codegen.extraction.type_analyzer import analyze_type

annotation = Building.model_fields["version"].rebuild_annotation()
shape, nullable, description = analyze_type(annotation, owner=Building)

# NewTypeShape(name='FeatureVersion', ref=..., inner=Primitive(base_type='int32', ...))
assert shape.name == "FeatureVersion"
assert shape.inner.base_type == "int32"
assert nullable is False

FieldShape is a tree, not a flat record: NewTypeShape, ArrayOf, and MapOf wrap an inner shape, and the three terminals (Primitive, LiteralScalar, AnyScalar) sit at the leaves. Nesting order is meaningful -- NewTypeShape(inner=ArrayOf(...)) is a NewType over list[X], while ArrayOf(element=NewTypeShape(...)) is a list of NewType-wrapped values.

Constraints attach to the layer they target and carry the NewType that contributed them:

for source in shape.inner.constraints:
    print(f"{source.constraint} from {source.source_name}")
# Ge(ge=0) from FeatureVersion
# Ge(ge=-2147483648) from int32
# Le(le=2147483647) from int32

Fetching sample data

Theme packages include example records in their pyproject.toml files under [[examples.<Type>]] sections. The codegen validates these against Pydantic models and renders them in feature pages.

To fetch a fresh sample from the latest Overture release using DuckDB:

duckdb -json \
  -c "load spatial" \
  -c "attach 'http://labs.overturemaps.org/data/latest.ddb' as overture" \
  -c "select to_json(columns(*))
      from (
        select * REPLACE ST_AsText(geometry) as geometry
        from overture.place
        USING SAMPLE 1
      )" \
  | jq .

The latest.ddb database always points to the current release. Tables use the type name directly (overture.place, overture.segment, overture.building, etc.). Convert the JSON output to TOML for inclusion in the theme's pyproject.toml.

Further reading

  • Design document -- architecture, extension points, data flow diagrams
  • Walkthrough -- module-by-module narrative tracing Segment through the full pipeline