Skip to content

fix(codegen/python): emit nullable Option<T> fields as optional in flattened tagged-enum models#170

Merged
hardbyte merged 1 commit into
mainfrom
fix/python-codegen-flattened-optional-fields
Jun 11, 2026
Merged

fix(codegen/python): emit nullable Option<T> fields as optional in flattened tagged-enum models#170
hardbyte merged 1 commit into
mainfrom
fix/python-codegen-flattened-optional-fields

Conversation

@hardbyte

Copy link
Copy Markdown
Contributor

Fixes #167

Root cause

The bug is one step removed from the issue's minimal repro: a standalone internally-tagged enum already generates correctly, because that render path routes field optionality through resolve_field_optionality (which treats any Option<T> field as an optional Python key, since serde omits the key for None).

The failing shape is a struct with a #[serde(flatten)]-ed internally-tagged enum — e.g. OffersSearchOffer flattening an offer-details enum, producing per-variant models like OffersSearchOfferProvisional. Those models are generated by render_struct_with_flattened_internal_enum / render_struct_with_flatten_standard, which hand-rolled optionality from field.required alone and ignored the Option<T> special case. Two symptoms:

  • Option<T> without skip_serializing_ifamount: str | None required, no defaultpydantic.ValidationError: Field required on a valid payload that omits the key
  • Option<T> with skip_serializing_if → doubled union note: str | None | None = None

@MattGson note: your minimal repro only fails when the enum is flattened into a parent struct — worth sanity-checking that matches the offers code shape.

Fix

Route all five inline field-emission sites in reflectapi/src/codegen/python.rs through resolve_field_optionality:

  • parent-struct base fields and flattened-sibling-enum fields in render_struct_with_flattened_internal_enum
  • tuple-variant inner-struct fields in the same function
  • regular fields in render_struct_with_flatten_standard
  • flattened-enum-as-field emission (had its own double-| None bug for Option<Enum>)

Field optionality is now decided in exactly one place, so flattened-enum variant models match standalone struct output.

Tests

  • New snapshot samples for both shapes: the issue's standalone repro (guards the already-correct path) and the flattened variant that reproduces the bug. Both run through assert_snapshot!, covering schema JSON, TypeScript, Rust, OpenAPI, and Python.
  • Per @avkonst's suggestion, python codegen is now covered by the same test samples as the other languages: it was already in assert_input_snapshot!/assert_output_snapshot!/assert_snapshot!; the one gap was assert_builder_snapshot! (the 4 namespace samples), which now snapshots python too (appended last so existing snapshot indices don't churn).
  • 5 existing generic_flatten_* python snapshots updated — each is the same one-line correction (if_else: X | None gaining = None); those committed snapshots had been silently capturing the bug.
  • Runtime-validated the generated client under pydantic with the local reflectapi-python-runtime: model_validate({"type": "variant", ...}) with omitted optional keys now succeeds on variant models directly and through the discriminated-union root, and populated keys still parse.
  • Full demo suite passes (193/193); fmt/clippy/build clean.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

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

Updated automatically from commit c02e9fa

…attened tagged-enum models

The per-variant model paths for structs with a flattened internally-tagged
enum (and the standard flatten path) hand-rolled field optionality from
`field.required` alone, ignoring the Option<T> special case that
resolve_field_optionality already implements. As a result a nullable field
whose key serde omits when None was emitted as a required key
(`amount: str | None`, no default), so validating a valid payload raised
`pydantic.ValidationError: Field required`. Fields that were not required
got a doubled union (`str | None | None`).

Route all five field-emission sites through resolve_field_optionality so
flattened-enum variant models match standalone struct output.

Also add python codegen snapshots to assert_builder_snapshot! so the
namespace test samples cover python like the other languages, and add
repro samples for both the standalone and flattened internally-tagged
enum shapes.

Fixes #167
@hardbyte hardbyte force-pushed the fix/python-codegen-flattened-optional-fields branch from d642e67 to 5876dff Compare June 11, 2026 02:54
@hardbyte hardbyte merged commit a4da6f0 into main Jun 11, 2026
6 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.

Bug: reflectapi Python codegen emits a nullable Option<T> field as a required key

2 participants