OvertureMaps / OvertureMaps/schema

[ENHANCEMENT](schema) Remove the non-null defaults from the schema models

Open
#696 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

Companion to #695, which adopts the policy that Overture schema fields carry no non-null default. That issue is the why; this one removes the ones that exist.

Eleven fields carry a non-null default. They resolve to four declaration sites in four files -- the rest are inherited and re-reported by subclasses.

#692 already did this once, removing the connectors = [] default and making the field required. These four are what remain.

Verified on origin/vnext at b893ab09, which carries #692; origin/main does not yet.

Declaration Default Fields it produces Becomes
common/level.py:23 Stacked.level 0 6 Level | None = None
transportation/segment/_common.py:318 SpeedLimitRule.is_max_speed_variable False 1 bool | None with no default
base/land.py:142,145 Land.class_, Land.subtype LandClass.LAND, LandSubtype.LAND 2 required
base/water.py:141,148 Water.class_, Water.subtype WaterClass.WATER, WaterSubtype.WATER 2 required
Land and Water class and subtype

Published data already satisfies the tightened constraint, the same justification #692 used for connectors. In release 2026-08-19.0, one partition per type:

Type Rows subtype null class null
land 1,174,629 0 0
water 1,985,644 0 0

No published feature relies on the default.

subtype = 'water' reads as the identity of the model already chosen, which is what makes the default look harmless. It is not an identity: in the water partition it is one of 8 values, covering 1,200,960 of 1,985,644 rows, and in land, class = 'land' covers 68,679 of 1,174,629. They are catch-all values that happen to be common. Required is the right shape, not defaulted.

SpeedLimitRule.is_max_speed_variable

In the segment partition measured, false appears in 0 of 601,764 speed limit rules; 601,497 are null and 267 are true. Absent already means "not a variable speed corridor" to every reader of the release, and the description should say so rather than leaving a default to imply it.

With the default in place, absent, null, and false are three encodings of two meanings, and which pair collapses depends on whether you came through Pydantic or through Parquet.

After removing a default, fix the description

A default often carries meaning that nothing else states. Remove it without updating the field description and that meaning is gone silently. level and is_max_speed_variable both need a description of what absence means; class_ and subtype need nothing, since becoming required leaves no absence to explain.

The level sentence belongs on the Stacked.level field annotation, not on the Level NewType. Level is also the type of LevelRule.value (transportation/segment/_common.py:135), which is required -- absence is impossible there, so a NewType-level sentence about absence renders as nonsense into the segment JSON Schema and Markdown reference.

The description is what resolves this, and that is the whole point of the step. An absence with nothing said about it is itself a default -- the reader supplies a meaning, and different readers supply different ones. Removing a default without describing absence relocates the ambiguity instead of removing it.

is_max_speed_variable is where that bites. Absence could mean false, or could mean unknown, and the two make different claims about roads nobody surveyed. The published data bears on it without settling it: 267 rules say true, 601,497 say nothing, and none say false, so the publisher records only positives -- and that is not an artefact of the Pydantic default, since the data pipeline never consults it. Writing the description is the act of deciding, so it needs whoever knows what the flag asserts rather than an inference from the counts.

Breaking change classification

No consumer sees a difference: the release already carries what the models will require, and nobody reads Overture data through the Python models today.

The model contract still changes, so this is a breaking package change and lands in the 2.0.0 window.

Change Effect on the release Change to the model contract
level default dropped none; already null on all but a handful of rows input that omits level parses to None instead of 0
class_ / subtype required none; always written constructing Land() / Water() without those arguments raises
is_max_speed_variable default dropped none; false never written input that omits it parses to None instead of False

Nothing changes for anyone querying Parquet. Requiredness never reaches the emitted schema -- schema_builder.py:140 renders every field as StructField(name, type, True), nullable regardless of is_required -- so a required field is a validation rule, not a physical constraint. The columns and their nullability are identical before and after. The only difference is that validation now reports a null where it previously accepted one, and no published row has one.

Downstream effects

  • Markdown reference. class_ and subtype move from Optional to Required for Land and Water. Expected; confirm the rendered tables read correctly.
  • Generated PySpark. Required-field checks appear for the four newly-required fields. Regenerate and read the diff rather than accepting it.
  • JSON Schema output. The default annotations disappear, and class/subtype join the required arrays. Confirm the annotations are gone rather than relocated.

Acceptance criteria

  • No schema field carries a non-null default.
  • Every meaning a removed default carried is stated in the field description, in the same change.
  • Changelog fragments filed under each affected package.
  • A published partition from 2026-08-19.0 per touched type validates clean against the changed models, with overture-validate --skip-schema-check to clear the unreleased-field gate.
  • make check green; regenerated PySpark and Markdown diffs reviewed, not just regenerated.

Related

  • #695 -- the policy this implements.
  • #692 -- removed the connectors = [] default and made the field required; merged to vnext, and the precedent for these four.
  • #669 -- the connectors = [] correctness bug that motivated #692.
  • #640 -- default is the Field() kwarg decision this pair of issues resolves.
  • #354 -- the operating_status precedent: default removed, replacement never landed.

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 with common/level.py, transportation/segment/_common.py, base/land.py, and base/water.py, then inspect schema_builder.py:140 and the generated Markdown, PySpark, and JSON Schema diffs. Confirm the affected descriptions and requiredness, validate a 2026-08-19.0 partition with overture-validate --skip-schema-check, and run make check; done means the acceptance criteria and reviewed generated outputs pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.