Skip to content

Map OpenAPI 3.1 multi-type unions to any - #2522

Open
bendrucker wants to merge 1 commit into
oapi-codegen:mainfrom
bendrucker:union-any-type
Open

Map OpenAPI 3.1 multi-type unions to any#2522
bendrucker wants to merge 1 commit into
oapi-codegen:mainfrom
bendrucker:union-any-type

Conversation

@bendrucker

Copy link
Copy Markdown

OpenAPI 3.1 allows type to be a list, so a value may be any one of several types. The primitive-type dispatch in pkg/codegen/schema.go uses Types.Is(...), which only matches a single-element list, so every multi-type union fell through to unhandled Schema type and failed generation. Closes #2521.

Go has no type meaning "one of these", so a union maps to any. That follows the bare type: "null" branch directly above it, added for #2430, on the same reasoning: prefer the permissive mapping over rejecting an otherwise valid spec.

The mapping is deliberately narrow so the existing error keeps catching malformed input instead of quietly widening it. #1977 asks for a better error here, and swallowing more cases would be the wrong direction. A list-valued type is 3.1-only syntax, so a 3.0 document carrying one still fails. Every entry must name a JSON Schema type, so type: [strng, number] still fails on the typo.

One interaction needed handling. A union carrying an enum reached the enum branch, which emits const X T = ... against a type that is now any. That is not a valid Go constant, so generation succeeded but the output did not compile. Unions are now excluded from enum codegen for the reason type: array already is, and generate the plain any instead.

Nullable objects are unaffected. schemaPrimaryType strips "null" before the dispatch, so type: [object, "null"] still generates its struct. type: [object, string] does collapse to any and drops its declared properties. That is inherent to the mapping, since no Go type is either a struct or a string.

Generation is exercised across property, additionalProperties, parameter, request body, response body, array items, and allOf positions. One rough edge worth naming: a response body typed any under strict-server emits a method on a named interface type, which does not compile. That reproduces on main today for both schema: {} and type: "null", so this PR routes one more spec shape into it rather than causing it. Happy to fix that separately if you'd like it tracked.

For motivation, Honeycomb's published 3.1 spec carries two of these for genuinely polymorphic event and query-result values. The only workaround today is an overlay that strips the type keyword before generation.

OpenAPI 3.1 allows `type` to be a list, so a value may be any one of
several types. The primitive-type dispatch uses `Types.Is(...)`, which
only matches a single-element list, so every multi-type union fell
through to `unhandled Schema type` and failed generation.

Go has no type matching that constraint, so map a union to `any`, the
same permissive mapping a bare `type: "null"` already gets (oapi-codegen#2430),
rather than rejecting an otherwise valid spec.

The mapping is narrow on purpose, so the existing error still catches
malformed input. A list-valued `type` is 3.1-only syntax, so a 3.0
document carrying one keeps failing. Every entry must name a JSON Schema
type, so a misspelled one (`type: [strng, number]`) keeps failing too.

A union carrying an `enum` is excluded from enum codegen for the reason
`type: array` already is: `const X any = ...` is not a valid Go
constant. It generates the plain `any` the union maps to.
@bendrucker
bendrucker requested a review from a team as a code owner August 14, 2026 17:18
@greptile-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR allows valid OpenAPI 3.1 schemas with multiple declared JSON types to generate as permissive Go any values.

  • Adds version-aware recognition of valid multi-type lists while preserving errors for OpenAPI 3.0 and unknown type names.
  • Prevents union enums from producing invalid Go constants.
  • Adds unit, integration, generated-output, and README coverage for the new mapping.

Confidence Score: 5/5

The PR appears safe to merge, with no unacknowledged actionable defects identified in the changed behavior.

The new dispatch is restricted to valid OpenAPI 3.1 multi-type lists, preserves existing malformed-input errors, and prevents enum metadata from reaching invalid constant generation.

Important Files Changed

Filename Overview
pkg/codegen/schema.go Adds narrowly scoped OpenAPI 3.1 multi-type detection, maps recognized unions to any, and bypasses scalar enum generation for those unions.
pkg/codegen/schema_test.go Covers scalar and array-containing unions, null stripping, malformed type names, and the OpenAPI 3.1 version gate.
internal/test/openapi31/spec.yaml Extends the existing OpenAPI 3.1 fixture with union-valued maps, properties, named schemas, nullable unions, and enum-bearing unions.
internal/test/openapi31/openapi31.gen.go Representative generated output matches the intended any, map[string]any, and alias mappings without unrelated drift.
internal/test/openapi31/openapi31_test.go Verifies generated union values compile, accept multiple runtime types, and round-trip through JSON where applicable.
README.md Documents that OpenAPI 3.1 multi-type schemas map to Go any.

Reviews (1): Last reviewed commit: "Map OpenAPI 3.1 multi-type unions to `an..." | Re-trigger Greptile

@jamietanna jamietanna left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@mromaszewicz I'd expect that we'd treat this as a oneOf in this case?

@mromaszewicz mromaszewicz added the bug Something isn't working label Aug 15, 2026
@mromaszewicz

Copy link
Copy Markdown
Member

@mromaszewicz I'd expect that we'd treat this as a oneOf in this case?

You know, I'm not sure. This isn't like oneOf because that can handle any schema definition, however, this is much more limited, because it's just top level types. I do think any, like in this PR, is a better fit. If someone wants something that is [string, array, number], it's on them to disentangle what this is. any will have the json marshaler parse this as it sees fit, while if we make it something like json.RawMessage, then the user can decide how to parse it, so to me, it's a choice between any and json.RawMessage. I'm leaning towards any.

@mromaszewicz

Copy link
Copy Markdown
Member

Thanks for the PR — the core mapping is well-guarded and the writeup is unusually thorough. We ran a deep analysis with Claude on this change (probing generated output for the interaction cases against a compiled runtime), and it surfaced a few issues that make the change incomplete as-is. Findings below, roughly in order of severity.

1. Parameter positions generate code that always fails at request time

A multi-type union in a parameter now generates successfully, but the endpoint is permanently broken:

parameters:
  - name: id
    in: path
    required: true
    schema:
      type: [string, integer]

The generated handler binds into an any destination, and runtime.BindStyledParameterWithOptions / BindQueryParameter unconditionally reject that:

error binding string parameter: can not bind to destination of type: interface

So a path param typed this way 400s on every request, and a query param 400s whenever it's supplied. Before this PR the spec failed loudly at generation time; after it, the failure moves to runtime. Unlike type: "null", a string-or-integer ID is a plausible real spec, and this is the one position where any isn't merely permissive but non-functional. We'd want parameter schemas to either keep erroring at generation, or bind the raw string — not silently generate a dead endpoint.

Relatedly, the PR body says generation is exercised across "property, additionalProperties, parameter, request body, response body, array items, and allOf positions", but the committed suite only covers property, additionalProperties, a named component, and the enum interaction. If you tested the other positions locally, could you commit those tests? The parameter case above is exactly the kind of thing that coverage would have caught.

2. type list + oneOf/anyOf siblings: the composition is silently discarded

UnionWithOneOf:
  type: [string, number]
  oneOf:
    - type: string
    - type: number

generates type UnionWithOneOf = any with no union accessors, because generateUnion only runs inside the object/typeless branch of GenerateGoSchema. Not a regression (this errored before), but it's a legal 3.1 conjunction that now silently loses the As…/From… machinery a bare oneOf gets. At minimum this deserves a line in the README note.

3. allOf ordering now swallows unions silently

AllOfUnionMember:
  allOf:
    - properties:
        name:
          type: string
    - type: [string, number]

generates a plain struct — the union member's type vanishes, because MergeSchemas does result.Type = s1.Type and discards s2's type when s1 has none. Reverse the member order and you get any with the properties dropped instead. The order-dependency is a pre-existing MergeSchemas bug (single types behave the same on main), so it's not this PR's fault — but the PR converts some of these from hard errors into silent, order-dependent output. Worth either a guard or a tracking issue.

Confirmed as fine

  • The strict-server any-response compile failure reproduces on main with schema: {}, exactly as the PR body says — pre-existing, not introduced here.
  • Nullability handling is correct throughout: [string, number, "null"] properties render bare any without omitempty, and nullable.Nullable[any] under the nullable-type option.
  • The enum guard is correctly placed, the 3.0/typo rejection works as described, and x-go-type still wins as an escape hatch since extensions are handled before type dispatch.
  • A union as a oneOf branch works (accessor typed any), with the caveat that As… can never fail, so branch discrimination degrades — same as typeless branches today.

Overall: the schema-position mapping looks merge-worthy, but we'd like the parameter story resolved (and the position tests committed) before this lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-type unions aren't handled for OpenAPI 3.1

3 participants