OvertureMaps / OvertureMaps/schema

[ENHANCEMENT (BUG?)] Introspection breaks on `segment`: no supported way to ask a feature type what fields it has

Open
#722 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
213
Forks
22
Avg merge
3d 3h
Merged PRs (30d)
31

Description

Type

Task

Scope

Multi-theme or Platform

Skillset

engineering

Description
The question

Someone installs the packages and asks a feature type which fields it has and which of them are required. For 14 of our 15 feature types, the standard Pydantic idiom gets the answer:

from overture.schema.buildings import Building

sorted(n for n, f in Building.model_fields.items() if f.is_required())
# ['geometry', 'id', 'theme', 'type', 'version']

model_fields and FieldInfo.is_required() are both public Pydantic API.

Where it breaks

None of this works for segment:

from pydantic import TypeAdapter
from overture.schema.transportation import Segment

Segment.model_fields
# AttributeError: model_fields

Segment()
# TypeError: Cannot instantiate typing.Union

TypeAdapter(Segment).validate_python({})
# 1 validation error for tagged-union[function-after[__validate_ext_fields__(),
#   function-wrap[__validate_with_geo_json_support__()]], ...

Segment is a discriminated union alias over RoadSegment, RailSegment, and WaterSegment, so it isn't a model class and has no fields of its own.

Why Pydantic doesn't handle this the way we might want

TypeAdapter is the supported way to work with a union alias, and it does answer some questions. TypeAdapter(Segment).json_schema() returns a document with a three-branch oneOf, and validate_python and validate_json both work fine.

But a TypeAdapter exposes only validation, serialization, and JSON Schema. There's no model_fields on it and no field-level introspection of any kind. So the only structural answer Pydantic can give us for segment is a JSON document, and a document you feed to another program isn't an answer to "what fields does this type have." It's also several hundred lines for one feature type, since every enum and shared structure gets inlined.

Workaround

Import the three segment types and ask each one:

from overture.schema.transportation import RoadSegment, RailSegment, WaterSegment

for cls in (RoadSegment, RailSegment, WaterSegment):
    print(cls.__name__, sorted(n for n, f in cls.model_fields.items() if f.is_required()))

# RoadSegment  ['class_', 'connectors', 'geometry', 'id', 'subtype', 'theme', 'type', 'version']
# RailSegment  ['class_', 'connectors', 'geometry', 'id', 'subtype', 'theme', 'type', 'version']
# WaterSegment ['connectors', 'geometry', 'id', 'subtype', 'theme', 'type', 'version']

This works only if you already know those three types exist and what they're called. Getting to them from Segment takes two rounds of typing.get_args, and they come back as Annotated aliases whose __name__ is 'Annotated', so it takes a third round to recover the class names. Nobody will discover that.

The three also disagree, which is the interesting part. class is required on road and rail segments and doesn't exist on water segments. Seven fields are required on all three, and twelve of the twenty-three fields appear on all three.

Proposal

A small introspection API in overture-schema-system, alongside discover_models and filter_models. It SHOULD accept a model class, a union alias, or a registered type name, so that RoadSegment, Segment, and "segment" are all valid arguments:

required_fields(Building)    # ('id', 'geometry', 'theme', 'type', 'version')
required_fields("segment")   # see open question 1
fields("place")              # every field, with required flag, type, and constraints

The codegen extraction layer already computes most of this to render the markdown reference, so this may be a matter of exposing what exists rather than writing something new.

Open questions
  1. What does a union return? For segment, the fields common to all three types and the fields present on any of them differ by exactly one, class. The common set answers "what can I rely on for any segment." A per-type breakdown answers "what exists." We MAY need both.
  2. Where does it live? overture-schema-system next to discovery, or overture-schema-validation?
  3. Should the CLI answer this too? A user at the shell has no way to ask at all right now. overture-schema describe segment may be the more valuable half of this issue, since it serves people who don't write Python and gives the docs something to link to.
  4. How far does it go? Required fields only, or fields plus constraints plus enum values? The narrow version is easy to agree on and easy to ship. The broad version overlaps codegen and needs a design pass.

Verified against 2.0.0 from PyPI, Pydantic 2.13.5.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing overture-schema-system's discover_models and filter_models, then inspect the codegen extraction layer that renders the markdown reference. Compare the proposed behavior for model classes, union aliases, and registered names, especially the open question about common versus per-type union fields. Done means the API scope and location are agreed and the selected introspection behavior is exposed; add the CLI only if that scope is chosen.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design, developer-experience
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.