Skip to content

Detect partial enum idiom#2418

Draft
mromaszewicz wants to merge 1 commit into
oapi-codegen:mainfrom
mromaszewicz:fix/issue-2412
Draft

Detect partial enum idiom#2418
mromaszewicz wants to merge 1 commit into
oapi-codegen:mainfrom
mromaszewicz:fix/issue-2412

Conversation

@mromaszewicz

Copy link
Copy Markdown
Member

Closes: #2412

An anyOf/oneOf of a bare primitive and that same primitive constrained by an enum ("a value from this enum, or any other value of the same type") previously rendered as a JSON union -- or, for a top-level component, as nothing at all -- discarding the enum's known values.

Detect this idiom and instead resolve the schema to the bare primitive (kept as a type alias, so any value stays assignable without a conversion) while still emitting the enum's known values as untyped constants. No distinct enum type and no Valid() method are generated, since any value of the primitive type is legal.

Detection is deliberately strict: it fires only on a oneOf/anyOf with exactly two inline primitive members of the same type and format, where exactly one carries an enum. Anything else -- a $ref member, three or more members, mismatched type/format, or allOf -- falls through to the existing union/merge behavior unchanged.

Implemented in GenerateGoSchema via untypedEnumUnion(); enum-value construction is factored into buildEnumValues() and shared with the existing enum branch. EnumDefinition gains ConstType and SkipValidate so constants.tmpl can emit primitive-typed constants and skip Valid() for this case. Adds the issue-2412 regression fixture.

What's broken: this is the same idiom #1189 fixed. At the property level, current releases generate a union wrapper (As*/From*/Merge* accessors); collapsing to a plain string removes that API, which is a breaking change for existing users. It is additive only for top-level components (which generate nothing today). For that reason the behavior is not yet gated -- it likely needs an opt-in output-options flag (default off) to preserve the #1189 union representation, to be revisited. See the discussion on #2412. The issue-1189 golden file is regenerated to reflect the new output.

Closes: oapi-codegen#2412

An anyOf/oneOf of a bare primitive and that same primitive constrained by
an `enum` ("a value from this enum, or any other value of the same type")
previously rendered as a JSON union -- or, for a top-level component, as
nothing at all -- discarding the enum's known values.

Detect this idiom and instead resolve the schema to the bare primitive
(kept as a type alias, so any value stays assignable without a
conversion) while still emitting the enum's known values as untyped
constants. No distinct enum type and no Valid() method are generated,
since any value of the primitive type is legal.

Detection is deliberately strict: it fires only on a oneOf/anyOf with
exactly two inline primitive members of the same type and format, where
exactly one carries an enum. Anything else -- a $ref member, three or
more members, mismatched type/format, or allOf -- falls through to the
existing union/merge behavior unchanged.

Implemented in GenerateGoSchema via untypedEnumUnion(); enum-value
construction is factored into buildEnumValues() and shared with the
existing enum branch. EnumDefinition gains ConstType and SkipValidate so
constants.tmpl can emit primitive-typed constants and skip Valid() for
this case. Adds the issue-2412 regression fixture.

What's broken: this is the same idiom oapi-codegen#1189 fixed. At the property level,
current releases generate a union wrapper (As*/From*/Merge* accessors);
collapsing to a plain string removes that API, which is a breaking change
for existing users. It is additive only for top-level components (which
generate nothing today). For that reason the behavior is not yet gated --
it likely needs an opt-in output-options flag (default off) to preserve
the oapi-codegen#1189 union representation, to be revisited. See the discussion on
oapi-codegen#2412. The issue-1189 golden file is regenerated to reflect the new
output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mromaszewicz

Copy link
Copy Markdown
Member Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds special handling for partial-enum primitive unions in generated Go models. The main changes are:

  • Detects strict two-member oneOf/anyOf primitive-plus-enum schemas.
  • Emits primitive aliases with known enum values as constants.
  • Skips Valid() methods for these open-ended enum cases.
  • Adds issue 2412 fixtures and updates the issue 1189 golden output.

Confidence Score: 3/5

These issues should be fixed before merging.

  • Formatted string unions can generate Go code that does not compile.
  • Nullable primitive union members can be collapsed into non-nullable Go types.
  • The happy-path string enum behavior is covered, but these edge cases affect the new core detector.

pkg/codegen/schema.go

Important Files Changed

Filename Overview
pkg/codegen/schema.go Adds partial-enum union detection and shared enum-value construction; the detector misses formatted-string and nullable edge cases.
pkg/codegen/codegen.go Uses primitive constant types and skips validation for untyped enums, relying on schema detection to pass only constant-capable Go types.
pkg/codegen/templates/constants.tmpl Renders enum constants with the new constant type and conditional validation method.

Reviews (1): Last reviewed commit: "Detect partial enum idiom" | Re-trigger Greptile

Comment thread pkg/codegen/schema.go
Comment on lines +874 to +878
if !isPrimitiveOAPISchema(a.Value) || !isPrimitiveOAPISchema(b.Value) {
return nil, false
}
// Same primitive type and format.
if !slices.Equal(a.Value.Type.Slice(), b.Value.Type.Slice()) || a.Value.Format != b.Value.Format {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Formatted strings break constants

This detector accepts every type: string member as a primitive, but some string formats resolve to Go types that cannot be constants. For an anyOf like format: byte plus format: byte with an enum value, this branch calls oapiSchemaToGoType and can set the untyped enum's Go type to []byte. GenerateEnums only quotes values when the Go type is exactly string, so the generated constant is typed as a slice and its value is emitted as an unquoted identifier. That generated code cannot compile. The special case should reject string formats whose resolved Go type is not constant-capable, or only enable this path when the resolved Go type stays string.

Comment thread pkg/codegen/schema.go
Comment on lines +877 to +890
// Same primitive type and format.
if !slices.Equal(a.Value.Type.Slice(), b.Value.Type.Slice()) || a.Value.Format != b.Value.Format {
return nil, false
}
// Exactly one member carries the enum constraint.
aHasEnum := len(a.Value.Enum) > 0
bHasEnum := len(b.Value.Enum) > 0
if aHasEnum == bHasEnum {
return nil, false
}
if aHasEnum {
return a, true
}
return b, true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Nullable member is dropped

This comparison only checks member type, format, and enum presence, so it collapses unions where the bare primitive branch is nullable: true and the enum branch is not. That schema accepts JSON null through the nullable branch, but this path returns the schema resolved from the enum-bearing member only, so the generated type becomes a plain non-nullable primitive. A required field or top-level schema using that union can no longer represent a valid null value. The detector should require matching nullable flags and preserve them, or reject nullable members from this special case.

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.

Allow anyOf with an enum (to indicate "a value in this enum or a plain string") to still render the enum

1 participant