Repository navigation
fix(codegen): read newtype structs as the value they wrap - #217
Conversation
There was a problem hiding this comment.
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.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 06ffc7f506
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
📖 Documentation Preview: https://reflectapi-docs-preview-pr-217.partly.workers.dev Updated automatically from commit 4459c9e |
Python picks how to render a flattened field from the type the field names. Behind a newtype or transparent struct, an enum was missed, so the model had a nested field (`event: Event`) where serde writes the tag and fields on the parent. Before #217 a newtype failed codegen instead; a transparent wrapper had this bug unless the schema was built with `fold_transparent_types()`. The struct is now rendered with each such field naming the wrapped type, which serde writes identically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ
Serde writes a newtype struct as the value it wraps, flattened or not. `Shape::Newtype` now says so in the two places that didn't: - OpenAPI described `struct UserId(Uuid)` as a one-element array (`prefixItems`), so a client generated from the spec sent `["..."]`. It is now the inner schema, as for a transparent struct. Unit structs, a newtype of `()` in the schema, are `null` instead of `[null]`. - `Layout` treated a flattened newtype as having unknown keys, so Python codegen failed and OpenAPI described the group with that array. It now contributes the keys of the type it wraps. The codegen coverage fixture flattens a required and an optional newtype, so CI replays serde's JSON through the generated Python models and checks their JSON schemas against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ
Python picks how to render a flattened field from the type the field names. Behind a newtype or transparent struct, an enum was missed, so the model had a nested field (`event: Event`) where serde writes the tag and fields on the parent. Before #217 a newtype failed codegen instead; a transparent wrapper had this bug unless the schema was built with `fold_transparent_types()`. The struct is now rendered with each such field naming the wrapped type, which serde writes identically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ
#219 and #220 added fixtures with newtype and unit structs, whose OpenAPI components were one-element arrays; they are now the wrapped value, like every other newtype here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ
…ield `Layout`, OpenAPI's struct schema and Python's flattened-wrapper rewrite each decided for themselves that a newtype, or a transparent struct with one field, is written as the field it wraps. `wrapped_field` in `codegen/schema/shape.rs` now decides it once. Generated output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ
debfe2a to
ad38c71
Compare
…8zj1 # Conflicts: # CHANGELOG.md
…8zj1 # Conflicts: # CHANGELOG.md
antonzherdev
left a comment
There was a problem hiding this comment.
LGTM and the agent also did not find anything real.
The next #129 slice after #212: "Newtype structs in
Layout". Building it turned up a wider bug in OpenAPI, which this also fixes. Rebased on main after #218–#221.Serde writes a newtype struct as the value it wraps, whether it is flattened or not:
struct UserId(Uuid)is"...", and#[serde(flatten)] w: Wrapperwithstruct Wrapper(Inner)writesInner's keys next to the parent's. Rust, TypeScript and Python clients already read newtypes this way. OpenAPI andLayoutdidn't.One rule for wrappers
wrapped_fieldincodegen/schema/shape.rssays which field a struct is written as when it wraps one: a newtype, or a#[serde(transparent)]struct with one field. Three call sites read it:LayoutBefore this PR,
Layoutand OpenAPI each handled transparent structs themselves, and neither handled newtypes. #219'snullability.rskeeps its own walk, because it also substitutes generic arguments and checks each field's codec.OpenAPI: a newtype struct is the schema it wraps
struct_to_schemadescribed a newtype struct as a one-element array,{"type": "array", "prefixItems": [T]}. A client generated from the spec sent[value], which the server rejects. The demo's ownpets.createrequest body,PetsCreateRequest(Pet), was[Pet]. It's now the inner schema, the same as a transparent struct.Unit structs change too. The schema describes
struct U;as a newtype of(), so OpenAPI showed it as[null], and it's nownull, which is what serde writes.Layout: a flattened newtype has the keys of the type it wrapsA flattened newtype was
Layout::Unknown, so:Now the layout follows the newtype into its inner type, as it already did for transparent structs. Python expands the inner keys onto the parent. OpenAPI writes
allOfwith the inner schema, or forOption<Wrapper>, aoneOfof the inner schema and "none of its keys".Python: a flattened wrapper of an enum renders as the enum
From Codex's review. Python decides how to render a flattened field from the type the field names. Behind a newtype or transparent struct it missed an enum, and generated a nested
event: Eventfield where serde writes the tag and fields on the parent.Layoutchange above would have turned main's codegen error into that wrong model.fold_transparent_types().Before rendering a struct, Python now rewrites each flattened field that names a wrapper to name the wrapped type. serde writes both forms identically.
Output
The Python, TypeScript and Rust demo clients, plus the enum compatibility and field presence fixtures, are byte-identical to main. Only OpenAPI output and flattened wrappers change. The codegen coverage fixture changes because this PR adds
SpanandParentto it.The changed snapshots are all OpenAPI ones:
basic__reflectapi_struct_newtype,basic__reflectapi_struct_unit_type,basic__reflectapi_struct_with_additional_derivesserde__unit_struct,serde__unit_tuple_structserde__struct_with_serde_default_named_typesandserde__struct_with_serde_default_none_types, both added by fix(codegen): decide null rejection from the shared field contract #219 and fix(python): an omittable unit field is annotated None once #220namespace__generic_and_type_conflictwrite_openapi_spec, and the checked-inreflectapi-demo/openapi.jsonIn each one a
prefixItemsarray becomes the wrapped value. Thewrapped_fieldrefactor changes no output: all 67 generated files are identical before and after it.Testing
serde__flatten_newtype_struct: a required and an optional flattened newtype, across all five outputs.WireRequestnow flattens a required newtypeSpanand an optionalParent, which alternatesSomeandNone. CI checks that serde's JSON reaches the transport unchanged through the generated Python client, round-trips, and validates against both Python JSON schemas.WireRequestschema withjsonschema: 6 of 6 valid on this branch, 0 of 6 on main ("is not of type 'array'").flattening_a_wrapper_of_an_enum_renders_the_enum: a newtype and a transparent wrapper of an internally tagged enum each render the sameOuterclasses as flattening the enum directly. It fails without the fix, for each wrapper.wrapped_field: newtype, one-field transparent, single named field and pair.Layout: a newtype struct has the keys of the struct it wraps.cargo fmt --check, clippy-D warnings --all-targets --all-features, andcargo test --workspace --all-featuresAlso in the changelog
#212 changed one case without an entry: a renamed single-field tuple variant,
A(#[serde(rename = "foo")] u32). It's listed under Unreleased now.Not changed
Renamed tuple fields in the derive. The derive stores a serde rename as a tuple field's name, although serde ignores it. Name-based checks (
is_named,is_tuple, TypeScript, Rust) then read it as a named field. On main and here alike, TypeScript codegen emits code that doesn't parse forstruct S(#[serde(rename = "foo")] u32). That's a separate fix.🤖 Generated with Claude Code
https://claude.ai/code/session_01VjkNsfHSd5osRevK95McpQ