Skip to content

Promote spec-defined fields to typed model fields (0.4.0)#31

Merged
phalt merged 6 commits into
mainfrom
claude/focused-mendel-cvq4n7
Jun 11, 2026
Merged

Promote spec-defined fields to typed model fields (0.4.0)#31
phalt merged 6 commits into
mainfrom
claude/focused-mendel-cvq4n7

Conversation

@phalt

@phalt phalt commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

This release promotes spec-defined fields that previously only appeared in model_extra to proper typed model fields across the OpenAPI spec models. Raw values are mirrored in model_extra for backwards compatibility (deprecated, to be removed in 0.5.0).

Key Changes

Schema Model

  • Added typed fields for JSON Schema / OpenAPI keywords: ref ($ref), format, enum, default, const, deprecated, nullable, read_only, write_only, example, examples, discriminator, additional_properties, and numeric/string/array/object constraint fields
  • Added has_default and has_const properties to distinguish explicit null values from absent fields
  • Added types property (always-a-list view of type keyword for OpenAPI 3.1 compatibility)
  • Added primary_type and is_nullable properties for unified 3.0/3.1 type handling
  • Implemented SpecModel base class with declarative from_dict() for nested field parsing

Operation Model

  • Promoted parameters to list[Parameter] (typed), responses to dict[str, Response] (typed)
  • Added typed fields: request_body, security, callbacks, deprecated, servers, external_docs
  • Implemented proper nested parsing for complex objects

Other Models

  • Parameter: Added ref, deprecated, allow_empty_value typed fields
  • Header: Added ref, deprecated typed fields
  • RequestBody/Response: Added ref typed field
  • MediaType: schema_ is now typed Schema instead of raw dict
  • Encoding: headers is now dict[str, Header] instead of raw dict
  • Link: Converted to snake_case fields (operation_ref, operation_id, request_body) with camelCase aliases; added typed server
  • PathItem: Added ref, summary, description typed fields; path-level parameters now typed
  • New Discriminator model: Represents OpenAPI discriminator objects with property_name and mapping

Query API Improvements

  • Added operations_by_tag(tag) to filter operations by tag
  • Added tags_to_operations() to group all operations by tag
  • Added __iter__() to Paths for direct iteration over (path, PathItem) pairs
  • Implemented reference caching in ReferenceResolver for performance

Backwards Compatibility

  • Raw values from promoted fields are mirrored into model_extra via new mirror_extras() helper
  • All models support populate_by_name=True for programmatic construction with snake_case names
  • Pre-0.4.0 access patterns like schema.model_extra.get("enum") continue to work (deprecated)

Infrastructure

  • Created SpecModel base class with declarative nested field parsing via NESTED_FIELDS and MIRRORED_EXTRA_KEYS class variables
  • Updated version to 0.4.0 in pyproject.toml
  • Comprehensive test coverage for all new typed fields and backwards compatibility

Notable Implementation Details

  • The from_dict() pattern in SpecModel separates nested field parsing (handled declaratively) from Pydantic validation, enabling proper type conversion while preserving raw values for mirroring
  • OpenAPI 3.1 type arrays are supported natively via type: str | list[str] with helper properties for unified access
  • Reference resolution is now cached per spec instance to improve performance on repeated lookups
  • Circular reference detection is preserved in the resolver

https://claude.ai/code/session_012uMV8n26pbFxhKLu83rTjE

claude added 6 commits June 11, 2026 10:28
- Schema: typed ref ($ref), format, enum, default, const, deprecated,
  nullable, readOnly/writeOnly, example/examples, additionalProperties
  (Schema | bool), discriminator, and numeric/string/array/object
  constraints. has_default/has_const distinguish 'default: null' from
  absent. Boolean JSON schemas (items: true) no longer crash parsing.
- New Discriminator model (propertyName + mapping) wired into Schema.
- Operation: typed parameters (list[Parameter]), responses
  (dict[str, Response]), request_body, security (None = inherit global),
  callbacks (dict[str, Callback]), deprecated, servers, external_docs.
- MediaType.schema_ is now a typed Schema instead of a raw dict.
- Parameter/Header: typed ref, deprecated, allowEmptyValue.
  RequestBody/Response: typed ref. Encoding.headers: dict[str, Header].
  Link: snake_case fields with camelCase aliases, typed server.
- PathItem: typed path-level parameters, summary, description, $ref;
  unknown keys now preserved in model_extra.
- ReferenceResolver reads the typed ref field before falling back to
  model_extra.
- Backwards compatibility: raw source values for all promoted keys are
  mirrored into model_extra via model_utils.mirror_extras (deprecated,
  to be removed in 0.5.0).
- Import cycle operation -> callback -> path_item -> operation broken
  with deferred bottom-of-module imports + model_rebuild.
- New test suites encode the typed-field and mirroring contracts
  (tests/spec/test_typed_fields.py, tests/spec/test_discriminator.py).

https://claude.ai/code/session_012uMV8n26pbFxhKLu83rTjE
- Schema.types: always-a-list view of the type keyword (3.0 string and
  3.1 type arrays).
- Schema.primary_type: first non-'null' type.
- Schema.is_nullable: unifies 3.0 'nullable', 3.1 type arrays containing
  'null', and anyOf/oneOf with a {'type': 'null'} member. Consumers no
  longer need to normalize 3.1 specs to 3.0 before parsing.
- OpenAPISpec.operations_by_tag() and tags_to_operations() for tag-based
  operation lookup.
- Paths is iterable as (path, PathItem) pairs and supports len().
- Reference resolution is now cached: one shared resolver per spec plus
  a per-resolver result cache; circular detection still consults the
  in-flight resolution stack first.
- Broken reference errors now include a did-you-mean suggestion and the
  available keys at the failure point.

https://claude.ai/code/session_012uMV8n26pbFxhKLu83rTjE
- New model_utils.SpecModel base class: shared extra=allow +
  populate_by_name config, declarative NESTED_FIELDS (object/collection/
  list kinds) driving a single generic from_dict, and automatic
  MIRRORED_EXTRA_KEYS handling.
- Migrated Example, MediaType, RequestBody, Response, Parameter, Header,
  Encoding, and Link to the base, deleting their hand-written from_dict
  methods. Schema subclasses SpecModel but keeps its custom recursive
  from_dict. Operation/PathItem keep their custom signatures.
- Malformed nested values of the wrong shape now degrade gracefully
  (skipped, field keeps its default) instead of crashing the parse.
- Type-check is now clean: ty exits 0 (8 warnings) vs failing with 124
  diagnostics on main.

https://claude.ai/code/session_012uMV8n26pbFxhKLu83rTjE
CHANGELOG documents the typed-field promotion with a migration table,
the deprecated model_extra mirroring (removal planned for 0.5.0), native
OpenAPI 3.1 nullability helpers, the new query API, and robustness
improvements. Docs updated for the new Schema/Discriminator fields,
Paths iteration, tag queries, and resolver caching/error behavior.

https://claude.ai/code/session_012uMV8n26pbFxhKLu83rTjE
- New model_utils.lenient_validator: malformed optional scalar values in
  real-world specs (e.g. YAML float for 'pattern', Swagger-2-style
  'required: true' on a property schema) fall back to the field default
  instead of rejecting the whole document. Applied to Schema, Parameter,
  Header, Response, RequestBody, Operation, and PathItem. Raw values stay
  available via model_extra (where mirrored) and OpenAPISpec.raw. This
  fixes a strictness regression versus 0.3.0, where these keywords lived
  unvalidated in model_extra.
- Schema.prefix_items: typed prefixItems (JSON Schema tuple validation,
  OpenAPI 3.1) with recursive parsing, extras mirroring, and __str__
  support. Covers the feature proposed in PR #30.
- SpecModel collection parsing now skips non-dict items (matching the
  list kind) instead of crashing on malformed entries.
- Simplified PathItem.from_dict: $ref/summary/description resolve via
  the plain splat and Pydantic aliases.
- New tests: tests/spec/test_lenient_parsing.py plus prefixItems cases.

https://claude.ai/code/session_012uMV8n26pbFxhKLu83rTjE
Operation-level inline content is now fully parsed (0.3.0 left it as raw
dicts), which exposed malformed data that previously went unvalidated:

- parse_collection: non-dict collections yield {}, non-dict items are
  skipped, and non-string keys (YAML parses unquoted 'no:' as boolean
  False) are coerced to str. SpecModel's collection kind now delegates
  to parse_collection instead of duplicating it.
- Operation.responses: status-code keys coerced to str.
- Example: lenient summary/description/externalValue (YAML bools/dates
  appear where strings belong in real specs).

Fixes the remaining 5 APIs.guru regressions (fec.gov, greip.io,
nordigen.com, vercel.com, vtex.local).

https://claude.ai/code/session_012uMV8n26pbFxhKLu83rTjE
@phalt phalt marked this pull request as ready for review June 11, 2026 13:38
@phalt phalt merged commit 803b7ff into main Jun 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants