Skip to content

fix: parse draft-04 tuple items and locate spec parse failures (#60) - #61

Merged
lightsofapollo merged 2 commits into
mainfrom
fix/tuple-items-and-parse-locations
Aug 26, 2026
Merged

fix: parse draft-04 tuple items and locate spec parse failures (#60)#61
lightsofapollo merged 2 commits into
mainfrom
fix/tuple-items-and-parse-locations

Conversation

@lightsofapollo

Copy link
Copy Markdown
Contributor

Summary

Fixes both halves of #60.

1. items: [A, B] crashed the parser. SchemaDetails.items only modeled the
JSON Schema 2020-12 single-schema form, so the draft-04 positional tuple form
failed the untagged Schema enum and took the whole document down. Tooling that
predates 2020-12 still emits it under openapi: "3.1.0" — FastAPI/pydantic v1
does. Both spellings are now modeled by an Items enum and unified with
prefixItems through SchemaDetails::positional_items().

2. The error named no node. Parse failures reported only serde's innermost
message — data did not match any variant of untagged enum Schema — with no
field, schema name, or position, so finding the culprit in a real spec meant
bisecting component schemas by hand. Failures now carry a JSON Pointer:

Failed to parse OpenAPI spec at #/components/schemas/Body/properties/pair/items:
data did not match any variant of untagged enum Schema

serde_path_to_error gives the document-level path, but serde's own tracking
stops at the first #[serde(flatten)] or untagged enum it buffers through
(#/paths for an inline schema), so the descent continues in
refine_schema_failure: through OpenAPI structure to real schema positions,
then keyword-first inside the schema that failed. Only nodes in a schema
position are tested — inferring from shape does not work, because a properties
map whose single property is named properties fails to parse as a schema while
being perfectly valid, and blaming it points the author at the wrong node
(there's a regression test for exactly that shape; github.json contains one).

On a 12 MB spec with a bad node injected deep in a path operation, the refined
pointer is exact and the whole failing run takes 0.7s. Refinement only ever runs
on the error path, and is capped at 20k parse attempts.

Generated compatibility

  • Generated model or method signatures: unchanged. A tuple generates exactly
    what prefixItems already generated (Vec<serde_json::Value> plus the
    minItems/maxItems doc comment). Emitting a real (String, String) for
    fixed-length tuples would be a better result for the users hitting this, but
    it changes generated APIs for every existing prefixItems spec — left as a
    follow-up covering both spellings.
  • Query/path/header/body wire behavior: unchanged, with one fix — the embedded
    Axum validator bundle now rewrites the tuple into prefixItems for 2020-12
    (and additionalItems into items). A 2020-12 validator ignores an
    array-valued items, so those declared positions previously went unchecked at
    runtime; specs using the tuple form now get 422s they should always have got.
  • Generated runtime dependencies or features: none. The generator itself gains
    serde_path_to_error (no transitive deps); generated crates are unaffected.
  • Configuration defaults or migrations: none.
  • Remaining unsupported OpenAPI shapes: positional item schemas are still not
    used for typing (same as prefixItems before this PR), and additionalItems
    is carried but unused. Line/column in parse errors is not included — YAML
    positions are lost in the serde_yaml::Valueserde_json::Value conversion
    and would need a position-preserving parse.

Validation

  • Added or updated a focused fixture and behavioral regression test.
    tests/tuple_items_test.rs: tuple/prefixItems output equivalence, the
    accessor unification, the validator bundle rewrite, pointer accuracy for a
    component schema and for an inline path schema, and the keyword-named
    property false-positive case.
  • Reviewed every changed snapshot; no unrelated churn is included (no
    snapshots changed).
  • cargo fmt --check
  • cargo clippy --all-features -- -D warnings
  • cargo test --all-features
  • RUSTDOCFLAGS=-Dwarnings cargo doc --no-deps --all-features
  • Ran scripts/install-smoke.sh for packaging/dependency changes.
  • Ran a targeted or full scripts/spec-compile.sh for generator changes
    (full 55-spec corpus).
  • Updated README, rustdoc, or changelog for user-visible behavior.

Notes for reviewers

Two commits, reviewable independently: the parse fix, then the error locating.

Smallest useful review path: Items and the two accessors in src/openapi.rs,
then refine_schema_failure / locate_failing_schema / deepest_schema_failure
at the bottom of src/analysis.rs. The five .items call sites in
src/analysis.rs are mechanical (&details.itemsdetails.item_schema()).

Risk is concentrated in the error-path walk: it is heuristic by nature and can
only ever change what a failing run says, never what a succeeding run
produces.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TD3TSeWKu4VqLtEnRMDjry

lightsofapollo and others added 2 commits August 26, 2026 10:58
`SchemaDetails.items` only modeled the JSON Schema 2020-12 single-schema
form, so a positional array failed the untagged `Schema` enum and took the
whole document down. Tooling that predates 2020-12 still emits the draft-04
tuple spelling under `openapi: "3.1.0"` — FastAPI/pydantic v1 does.

Model both spellings with an `Items` enum and unify them with `prefixItems`
through `SchemaDetails::positional_items`, leaving `item_schema` for the
single-schema form. Generated types are unchanged: a tuple generates exactly
what `prefixItems` already generated.

The embedded Axum validator bundle rewrites the tuple into `prefixItems` for
2020-12, where an array-valued `items` is ignored — without it the declared
positions went unchecked at runtime.

Refs #60

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD3TSeWKu4VqLtEnRMDjry
A document that failed to deserialize reported only serde's innermost
message — for the untagged `Schema` enum, "data did not match any variant of
untagged enum Schema" with no field, schema name, or position. Finding the
offending node in a real spec meant bisecting component schemas by hand.

Track the deserialization path with `serde_path_to_error`, then refine it:
serde's own path stops at the first `#[serde(flatten)]` or untagged enum it
buffers through (`#/paths` for an inline schema), so the descent continues
here — through OpenAPI structure to real schema positions, then keyword-first
inside the schema that failed.

Only nodes in a schema position are tested. Inferring from shape does not
work: a `properties` map whose single property is named `properties` fails to
parse as a schema while being perfectly valid, and blaming it would point the
author at the wrong node.

Errors now read:

    Failed to parse OpenAPI spec at
    #/components/schemas/Body/properties/pair/items:
    data did not match any variant of untagged enum Schema

Refs #60

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TD3TSeWKu4VqLtEnRMDjry
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openapi-to-rust Ready Ready Preview Aug 26, 2026 5:28pm

Request Review

@lightsofapollo
lightsofapollo merged commit ff503e7 into main Aug 26, 2026
12 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.

1 participant