Skip to content

feat(codegen): fold OpenAPI 3.1 const-based oneOf without outer type into enums - #2520

Open
sloudel wants to merge 2 commits into
oapi-codegen:mainfrom
sloudel:fix/enum-to-oneof-no-type
Open

feat(codegen): fold OpenAPI 3.1 const-based oneOf without outer type into enums#2520
sloudel wants to merge 2 commits into
oapi-codegen:mainfrom
sloudel:fix/enum-to-oneof-no-type

Conversation

@sloudel

@sloudel sloudel commented Aug 14, 2026

Copy link
Copy Markdown

Summary

OpenAPI 3.1 expresses string/integer enums as a oneOf whose branches each carry a scalar const. When the outer schema omits type (the common idiom, since the consts imply the family), detectEnumViaOneOf previously required an explicit outer scalar type and fell through to the union generator, emitting:

type PetStatus struct {
    union json.RawMessage
}
type PetStatus0 = interface{}

losing the enum values and names entirely.

This change lets the scalar family be inferred from the per-branch const values when the outer type is absent:

PetStatus:
  oneOf:
    - const: available
      title: Available
    - const: pending
      title: Pending
    - const: sold
      title: Sold

now produces a typed enum:

type PetStatus string
const (
    Available PetStatus = "available"
    Pending   PetStatus = "pending"
    Sold      PetStatus = "sold"
)

The integer variant (Port with whole-number consts) likewise emits Port int + numeric constants.

Behaviour

  • Outer type present — unchanged (must be string/integer to be treated as an enum).
  • Outer type absent — family inferred from the const values; every branch must agree and each const must be a plain scalar (string, or whole-number integer). Mixed/fractional/bool/object/array → falls through to the standard union path.
  • The detected type drives the Go base-type mapping via a shallow copy of the schema (the caller's schema is never mutated), so oapiSchemaToGoType yields string/int instead of falling into the generic interface{} branch.
  • SkipEnumViaOneOf escape hatch and the negative paths (missing title/const, object/mixed oneOf) still fall through to the union generator.

Tests

  • TestEnumViaOneOfConstType — unit coverage of the new inference helper (string / whole-number / fractional / bool / object / array / mixed).
  • Integration: PetStatus (string, no type) and Port (integer, no type) added to internal/test/openapi31/spec.yaml, with named-constant + JSON round-trip assertions.
  • Confirmed the existing with-type enums (Severity, Color) and the negative MixedOneOf case still pass.

Verification

go build ./...
go vet ./...
go test ./pkg/codegen/ -run TestEnumViaOneOfConstType
go test ./internal/test/openapi31/   # (run from internal/test module)

all green.

…into enums

OpenAPI 3.1 can express string/integer enums as a `oneOf` whose branches
each carry a scalar `const`, with the top-level schema omitting `type`
entirely (the consts imply the family). Previously detectEnumViaOneOf
required an explicit outer scalar `type`, so such schemas fell through to
the generic union path and emitted `type PetStatus struct { union
json.RawMessage }` plus a `PetStatus0 = interface{}` — losing the enum
values and names.

Now, when the outer `type` is absent, the scalar family is inferred from
the per-branch `const` values (string, or whole-number integer). Every
branch must agree and every const must be a plain scalar; otherwise the
union path is preserved. The detected type drives the Go base-type mapping
via a shallow schema copy, so a no-type const oneOf now emits e.g.
`type PetStatus string` with the named constants, instead of interface{}.

When the outer `type` is present, behaviour is unchanged (must be
string/integer to be treated as an enum). The SkipEnumViaOneOf escape
hatch and the negative paths (missing `title`/`const`, object/mixed
oneOf) continue to fall through to the union generator.

Adds a focused unit test for the inference helper and integration coverage
(PetStatus string no-type, Port integer no-type) with constants +
JSON round-trip, plus guards that the existing with-type enums still pass.
@sloudel
sloudel requested a review from a team as a code owner August 14, 2026 11:51
@greptile-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR infers string or integer enum types from OpenAPI 3.1 oneOf branch const values when the outer schema omits type.

  • Adds const-family inference and passes the inferred type through schema-to-Go mapping without mutating the input schema.
  • Adds unit and OpenAPI 3.1 integration coverage for string and integer inferred enums.
  • Regenerates the OpenAPI 3.1 fixture with the new enum declarations and constants.

Confidence Score: 4/5

The PR should not merge until inferred integer enums reject or correctly map constants that cannot be represented by their generated Go type.

A valid typeless oneOf containing a sufficiently large whole-number const is now folded into a Go int enum, producing an overflowing typed constant that causes generation to fail.

Files Needing Attention: pkg/codegen/schema.go

Important Files Changed

Filename Overview
pkg/codegen/schema.go Adds typeless const-based enum inference, but accepts integer constants that can overflow the generated Go integer type and abort generation.
pkg/codegen/schema_test.go Covers supported scalar families and rejected non-integral values, but omits integer representability boundaries.
internal/test/openapi31/spec.yaml Adds representative string and small-integer schemas using the new no-outer-type idiom.
internal/test/openapi31/openapi31_test.go Verifies generated constants and JSON round trips for the added fixtures.
internal/test/openapi31/openapi31.gen.go Generated changes are scoped to the expected PetStatus and Port enum output.
Prompt To Fix All With AI
### Issue 1
pkg/codegen/schema.go:697-700
**Integer inference permits overflowing constants**

When a typeless `oneOf` contains a whole-number const outside the generated Go integer type's range, this check still classifies it as an integer and emits it as a typed constant, causing code generation to fail while formatting the overflowing Go source.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(codegen): fold OpenAPI 3.1 const-ba..." | Re-trigger Greptile

Comment thread pkg/codegen/schema.go
Comment on lines +697 to +700
case float64:
if c == math.Trunc(c) {
return "integer", 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 Integer inference permits overflowing constants

When a typeless oneOf contains a whole-number const outside the generated Go integer type's range, this check still classifies it as an integer and emits it as a typed constant, causing code generation to fail while formatting the overflowing Go source.

Knowledge Base Used: Codegen Pipeline

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/codegen/schema.go
Line: 697-700

Comment:
**Integer inference permits overflowing constants**

When a typeless `oneOf` contains a whole-number const outside the generated Go integer type's range, this check still classifies it as an integer and emits it as a typed constant, causing code generation to fail while formatting the overflowing Go source.

**Knowledge Base Used:** [Codegen Pipeline](https://app.greptile.com/oapi-codegen/-/custom-context/knowledge-base/oapi-codegen/oapi-codegen/-/docs/codegen-pipeline.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

When the outer schema omits `type`, the scalar family is inferred from the
per-branch `const` values. A whole-number const that exceeds the Go int64
range (e.g. 2^63 or larger) was still classified as an integer and emitted
verbatim into a typed Go enum constant. Such a constant does not fit the
target integer type, so formatting the generated source failed.

Now the int64 bounds are enforced when inferring the integer family:

- float64 numbers must be whole AND in [-2^63, 2^63) (the high end is
  strict because math.MaxInt64 rounds up to 2^63 when widened to float64).
- integral defensive types (uint/uint64) are range-checked against int64.

Out-of-range consts fall through (ok=false) to the union generator instead
of producing an invalid typed enum. Adds unit coverage for the boundary
values and a detectEnumViaOneOf case proving an overflowing const falls
through.
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