Skip to content

refactor(codegen): share variant shapes between OpenAPI and Python - #212

Open
hardbyte wants to merge 2 commits into
mainfrom
brian/cool-maxwell-8p8zj1
Open

hardbyte wants to merge 2 commits into
mainfrom
brian/cool-maxwell-8p8zj1

Conversation

@hardbyte

@hardbyte hardbyte commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Another step towards #129, following #211. It picks up the review comment on #210:

The wire-to-JSON-Schema rules now exist twice. … Under the #129 direction, though, the variant shapes (unit/newtype/tuple/struct) should come from shared code next to #204's Layout, so both generators read the same facts.

How a variant appears on the wire follows from its shape in serde's data model and its enum's representation. OpenAPI, Python and the shared layout each worked out the shape from Fields themselves, and #210 added another copy for Python's JSON schemas. This PR defines it once, and every place in OpenAPI and Python that decides a shape now reads it.

The shape

codegen/schema/shape.rs, next to layout.rs:

Shape Fields Externally tagged wire
Unit none "A"
Newtype(&Field) one unnamed {"A": T}
Tuple(&[Field]) any other number of unnamed, including A() {"A": [T, U]} / {"A": []}
Struct(&[Field]) named, including A {} {"A": {...}} / {"A": {}}

Shape::of(&Fields) classifies; Shape::fields() lists the fields in order.

Now read from it

OpenAPI. variant_to_schema matches on Shape for every representation (external, adjacent, internal, untagged), replacing its Fields matches and fields.len() == 1 && !fields[0].is_named() checks.

Layout. The variants of an internally tagged enum.

Python (second commit for everything after the first item):

  • The externally tagged renderer. The (wire_name, shape) entries fix(python): describe the wire shape in JSON schemas of tagged and flattened enums #210 passes to the runtime's externally_tagged_json_schema come from Shape through one mapping, json_schema_shape, instead of a string written in each branch.
  • The other enum renderers: adjacently tagged, internally tagged and untagged.
  • Flattened enums:
  • render_enum's complex-variant check, which keeps counting an empty tuple variant A() as a unit (see below).
  • enum_unit_tag_set.
  • The newtype-struct checks in render_struct and type_ref_to_python_type. Struct::is_tuple() && fields.len() == 1 and Shape::Newtype agree on any schema whose unnamed fields are named by index, which reflection always produces.

What still matches Fields in Python only visits every field whatever its shape, or builds one:

  • collect_type_vars_from_type
  • validate_type_references
  • rename_type_params_in_type
  • infer_enum_generic_params
  • concrete_struct_from_type_ref
  • materialize_nested_policy_flatten

Shape adds nothing there; fields.iter() would be the tidy-up.

Output

Generated output is unchanged. Two existing quirks are kept on purpose, because fixing either changes output and belongs in its own PR:

  • OpenAPI: an empty externally tagged variant (B {}, C()) leaves its key out of required, although serde always writes it.
  • Python: render_enum counts A() as a unit, so enum E { A, B() } becomes a string enum with B = "B". Serde writes B() as {"B": []}, so the generated client can't read that variant.

Testing

  • Generated output. I generated the demo client (Python, TypeScript, Rust) and the codegen coverage, enum compatibility and field presence fixtures on main (53f50d1) and on this branch, after each commit. All 60 files are byte-identical, including fix(python): describe the wire shape in JSON schemas of tagged and flattened enums #210's serde samples.
  • Snapshots. No snapshot changed. They cover the paths changed here, including:
    • empty, newtype and untagged variants in OpenAPI
    • tuple[()] and root: None for untagged enums
    • adjacently tagged tuple content
    • flattened internally tagged variants
  • New tests:
    • Shape: a newtype vs a tuple (including A()), empty and non-empty structs, field order
    • the Python error for an internally tagged tuple variant, which also passes on main, so the message is unchanged
  • Passing locally:

Not changed

  • TypeScript still classifies Fields itself, in about 13 places; separate PR.
  • Mapping a shape to JSON Schema still exists twice: in openapi.rs, and in the runtime's serde.py, which rebuilds it from Pydantic's core schema. Removing that duplication would mean codegen emitting the schema fragments itself, which is a separate decision.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ

How a variant appears on the wire follows from its shape in serde's data
model (unit, newtype, tuple or struct) and its enum's representation.
OpenAPI, Python and the shared layout each worked out the shape from
`Fields` themselves; #210 added a fourth copy for Python's JSON schemas.

`codegen/schema/shape.rs` defines `Shape::of(&Fields)` once:

- Unit: no fields, `"A"` when externally tagged
- Newtype: one unnamed field, written as that field's value
- Tuple: any other number of unnamed fields, written as an array; `A()`
  is an empty tuple, not a unit
- Struct: named fields, written as an object; `A {}` is an empty struct

Now read from it:

- OpenAPI's `variant_to_schema`, for every representation
- the layout's variants of an internally tagged enum
- Python's externally tagged enum renderer, including the `(wire_name,
  shape)` entries #210 passes to the runtime's JSON schema helper, and
  the unit-only check for a flattened externally tagged enum

Generated output is unchanged: the demo client (Python, TypeScript, Rust)
and the codegen coverage, enum compatibility and field presence fixtures
are byte-identical to main, and no snapshot changed. OpenAPI's empty
externally tagged variants (`B {}`, `C()`) still leave their key out of
`required`, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

📖 Documentation Preview: https://reflectapi-docs-preview-pr-212.partly.workers.dev

Updated automatically from commit 09d54d4

The rest of Python's codegen that decides a shape now matches on the
shared Shape instead of on Fields:

- the adjacently tagged, internally tagged and untagged enum renderers
- the variants of a struct's flattened internally tagged enum, and
  variant_flattened_groups
- render_enum's check for complex variants, which keeps counting an empty
  tuple variant `A()` as a unit
- enum_unit_tag_set
- the newtype-struct checks in render_struct and type_ref_to_python_type

What remains matches Fields only to visit every field whatever its shape
(type variables, reference validation, type parameter renames, generic
inference) or to build one, so Shape adds nothing there.

`Struct::is_tuple() && fields.len() == 1` and `Shape::Newtype` agree on any
schema whose unnamed fields are named by index, which reflection always
produces.

Generated output is unchanged: the demo client and the codegen coverage,
enum compatibility and field presence fixtures are byte-identical to main,
and no snapshot changed. A new test pins the error for an internally
tagged tuple variant, which passes on main too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ
@hardbyte
hardbyte marked this pull request as ready for review October 30, 2026 02:55

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@hardbyte

Copy link
Copy Markdown
Contributor Author

@claude review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T02:57:15.862885Z 057921b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@hardbyte
hardbyte requested a review from antonzherdev October 30, 2026 04:56

This branch has not been deployed

No deployments
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