Skip to content

feat: emit typed wrapper for primitive oneOf schemas#549

Draft
plheide wants to merge 4 commits intoomissis:mainfrom
plheide:feat/p4-primitive-oneof
Draft

feat: emit typed wrapper for primitive oneOf schemas#549
plheide wants to merge 4 commits intoomissis:mainfrom
plheide:feat/p4-primitive-oneof

Conversation

@plheide
Copy link
Copy Markdown

@plheide plheide commented Apr 26, 2026

Currently, a schema with oneOf whose variants are all JSON primitives falls through to interface{} with no validation: the schema parser reads OneOf into the model but the generator never references it. This produces broken or empty output (see e.g. #187 for the related top-level-oneOf-of-objects case, which this PR does not fix).

Detects schemas where every oneOf variant declares a single primitive type (string, number, integer, boolean, null) with no nested constraints, and emits a small wrapper Go type instead of falling through to interface{}. The wrapper holds the decoded value in a private field and exposes:

  • UnmarshalJSON — dispatches on json.Decoder.Token kind, so a number that also matches integer cannot ambiguously satisfy two variants
  • MarshalJSON — emits the underlying value or null
  • UnmarshalYAML / MarshalYAML
  • Value() any — typed getter
  • IsZero() bool — supports omitzero
  • As<Kind>() — typed accessor per declared variant (AsString / AsNumber / AsBool / IsNull)

Detection wires into both generateDeclaredType (root) and generateTypeInline (object property) so the wrapper is emitted whether the oneOf is the root schema or a nested field. (Root-level oneOf-only schemas with no top-level type still hit the existing early return in generateRootType; that's a separate fix.)

Test coverage under tests/data/oneOfPrimitive/ covers number+string, number+string+bool, string+null, and the in-field case (object with a value property typed oneOf:[number,string,bool]). Runtime tests in tests/unmarshal_json_test.go cover acceptance per variant, rejection of disallowed kinds, and JSON round-trip.

Known limitation

integer and number variants both decode through case json.Number into float64. For schemas that allow integer and need to preserve int64 values ≥ 2^53, this loses precision and there is no AsInt accessor. Adding a distinct oneOfKindInteger is straightforward if needed; happy to roll it in here or as a follow-up.

Final part of the small series; depends on #548.

Related: #187 (this PR partially addresses oneOf in field position; root-level oneOf-of-objects requires general discriminated-union support, which is the planned follow-up).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 502d2e1c-6bb0-457d-8cd4-483a99906644

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 56.66884% with 666 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@d7e5ed1). Learn more about missing BASE report.

Files with missing lines Patch % Lines
tests/data/oneOfPrimitive/inField/inField.go 34.61% 57 Missing and 11 partials ⚠️
tests/data/oneOfPrimitive/withNull/withNull.go 33.33% 45 Missing and 5 partials ⚠️
tests/data/formatValidation/all/all.go 35.29% 22 Missing and 22 partials ⚠️
...ata/core/additionalProperties/autoinstallSchema.go 0.00% 32 Missing ⚠️
...pertiesAlways/addlOmittedEmpty/addlOmittedEmpty.go 39.47% 21 Missing and 2 partials ⚠️
tests/data/core/allOf/allOfMultipleRequired.go 0.00% 22 Missing ⚠️
tests/data/core/anyOf/anyOfMultipleRequired.go 0.00% 22 Missing ⚠️
tests/data/core/allOf/allOfWithDirectProperties.go 0.00% 20 Missing ⚠️
tests/data/core/anyOf/anyOfWithDirectProperties.go 0.00% 20 Missing ⚠️
pkg/generator/unmarshal_body.go 77.10% 14 Missing and 5 partials ⚠️
... and 53 more
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #549   +/-   ##
=======================================
  Coverage        ?   45.55%           
=======================================
  Files           ?       85           
  Lines           ?     7136           
  Branches        ?        0           
=======================================
  Hits            ?     3251           
  Misses          ?     3492           
  Partials        ?      393           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@plheide plheide force-pushed the feat/p4-primitive-oneof branch from 599e3dd to b747147 Compare April 26, 2026 21:32
@plheide plheide force-pushed the feat/p4-primitive-oneof branch 4 times, most recently from dd6ee06 to 24ab94a Compare May 5, 2026 20:59
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@plheide plheide force-pushed the feat/p4-primitive-oneof branch 2 times, most recently from bb53991 to 2b8da87 Compare May 5, 2026 21:49
plheide and others added 4 commits May 6, 2026 07:02
Both jsonFormatter.generate and yamlFormatter.generate were near-identical
near-100-line blocks. Extracts the shared body into generateUnmarshalBody
in a new pkg/generator/unmarshal_body.go and shrinks each formatter to a
~25-line wrapper that supplies an unmarshalContext describing only the
format-specific parts (function signature, decode call).

Adds a replacesUnmarshalBody bool to validatorDesc, honored by the helper
so that future validators (e.g. oneOf primitives) can take full control
of the function body. Currently unused but keeps the helper's contract
self-contained.

Behavior is preserved byte-for-byte: all golden-file generation tests
pass with no diff.
Adds Config.FormatValidation (off by default, with optional AllowList)
that turns on a stdlib-only formatValidator for the format keywords
the type mapping previously left as bare string: uuid, email, uri,
uri-reference, hostname, regex.

Each format compiles to a small post-decode check on the typed field
(net/mail.ParseAddress for email, net/url.Parse for uri, regexp for
the others). Nillable pointer fields are gated by a nil check so
absent optional values are not flagged.

Includes golden-file fixtures under tests/data/formatValidation/ and
20 round-trip assertions in tests/unmarshal_json_test.go covering both
acceptance and rejection.
Adds Config.StrictAdditionalProperties with three modes:

  off (default)        - silently drop unknown fields, preserving
                         historical behavior.
  respect-schema       - reject unknown fields only when the schema
                         declares additionalProperties: false.
  strict               - reject unknown fields for every generated
                         object type. Skipped when the schema declares
                         a typed additionalProperties (a catch-all map
                         field is generated instead).

Implementation is a strictFieldsValidator that reuses the existing
raw-map pre-validation seam in unmarshal_body.go. patternProperties
suppresses enforcement with a warning since it has no first-class
generator support.

Includes golden-file fixtures under tests/data/strictAdditionalProperties{,Always}/
and round-trip assertions covering accept/reject for both modes.
Detects schemas where every oneOf variant is a JSON primitive (string,
number, integer, boolean, null) and emits a wrapper Go type instead of
falling through to interface{}. The wrapper holds the decoded value in
a private field and exposes:

  UnmarshalJSON   - dispatch on json.Decoder.Token kind so a number
                    that also matches integer cannot ambiguously
                    satisfy two variants.
  MarshalJSON     - emit the underlying value or null.
  UnmarshalYAML
  MarshalYAML
  Value()         - typed getter returning any.
  IsZero()        - supports omitzero.
  As<Kind>()      - typed accessor per declared variant
                    (AsString / AsNumber / AsBool / IsNull).

Detection wires into both generateDeclaredType (root) and
generateTypeInline (object property) so the wrapper is emitted whether
the oneOf is the root schema or a nested field.

Includes golden-file fixtures under tests/data/oneOfPrimitive/ covering
number+string, number+string+bool, string+null, and an in-field case
that mirrors the original motivating use case (a "value" property
typed as number|string|bool).
@plheide plheide force-pushed the feat/p4-primitive-oneof branch from 2b8da87 to 6570abd Compare May 6, 2026 05:07
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