Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2321,6 +2321,8 @@ Got one to add? Please raise a PR!

Yes. Initial OpenAPI 3.1 support landed with [#2336](https://github.com/oapi-codegen/oapi-codegen/pull/2336), including [webhooks](https://spec.openapis.org/oas/v3.1.0#oasWebhooks) and version-aware handling of 3.1 idioms such as `type: [T, "null"]` nullability and enums declared via `oneOf` + `const`.

A 3.1 schema may also declare a multi-type union, such as `type: [string, number, boolean]`. Go has no type expressing that constraint, so these generate `any`.

If you're on an older release that predates this, you can [use OpenAPI Overlay](#modifying-the-input-openapi-specification-with-openapi-overlay) to "downgrade" an OpenAPI 3.1 spec to OpenAPI 3.0, following [steps from this blog post](https://www.jvt.me/posts/2025/05/04/oapi-codegen-trick-openapi-3-1/).

### How does `oapi-codegen` handle `anyOf`, `allOf` and `oneOf`?
Expand Down
2 changes: 1 addition & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
github.com/oapi-codegen/iris-middleware v1.0.5
github.com/oapi-codegen/nethttp-middleware v1.1.2
github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000
github.com/oapi-codegen/runtime v1.5.0
github.com/oapi-codegen/runtime v1.7.0
github.com/oapi-codegen/testutil v1.1.0
github.com/stretchr/testify v1.11.1
)
Expand Down
4 changes: 2 additions & 2 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ github.com/oapi-codegen/nethttp-middleware v1.1.2 h1:TQwEU3WM6ifc7ObBEtiJgbRPaCe
github.com/oapi-codegen/nethttp-middleware v1.1.2/go.mod h1:5qzjxMSiI8HjLljiOEjvs4RdrWyMPKnExeFS2kr8om4=
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
github.com/oapi-codegen/runtime v1.5.0 h1:aiil4QnH+eiWYSO60eaYZ4aur7sJH3rz6BvT5EBFnxc=
github.com/oapi-codegen/runtime v1.5.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/oapi-codegen/runtime v1.7.0 h1:t7358VYPvNbWJ9gdAkIK/smVeHpBf6yp8VTsaZsb/7k=
github.com/oapi-codegen/runtime v1.7.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/oapi-codegen/testutil v1.1.0 h1:EufqpNg43acR3qzr3ObhXmWg3Sl2kwtRnUN5GYY4d5g=
github.com/oapi-codegen/testutil v1.1.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
Expand Down
2 changes: 1 addition & 1 deletion internal/test/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/labstack/echo/v5 v5.1.0
github.com/oapi-codegen/nullable v1.1.0
github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000
github.com/oapi-codegen/runtime v1.5.0
github.com/oapi-codegen/runtime v1.7.0
github.com/oapi-codegen/testutil v1.1.0
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v3 v3.0.4
Expand Down
4 changes: 2 additions & 2 deletions internal/test/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
github.com/oapi-codegen/runtime v1.5.0 h1:aiil4QnH+eiWYSO60eaYZ4aur7sJH3rz6BvT5EBFnxc=
github.com/oapi-codegen/runtime v1.5.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/oapi-codegen/runtime v1.7.0 h1:t7358VYPvNbWJ9gdAkIK/smVeHpBf6yp8VTsaZsb/7k=
github.com/oapi-codegen/runtime v1.7.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/oapi-codegen/testutil v1.1.0 h1:EufqpNg43acR3qzr3ObhXmWg3Sl2kwtRnUN5GYY4d5g=
github.com/oapi-codegen/testutil v1.1.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
Expand Down
3 changes: 3 additions & 0 deletions internal/test/openapi31/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
// (one branch missing `title`) falls through to a plain union type alias.
// - the smaller 3.1 polish features: `const` on a scalar -> typed alias +
// singleton constant; plural `examples` -> Go doc comments.
// - multi-type unions (`type: [string, number, boolean]`) -> `any`, since Go
// has no type expressing "one of these". A "null" entry is the nullability
// marker and is stripped before the union check.
//
// Features that merely exist in both 3.0 and 3.1 (e.g. nullable) are NOT here --
// they live in their feature category with mixed-version specs.
Expand Down
16 changes: 16 additions & 0 deletions internal/test/openapi31/openapi31.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions internal/test/openapi31/openapi31_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,54 @@ func TestPetExampleComments(t *testing.T) {
"Lives field should surface the integer example as a doc fragment")
}

// ----------------------------------------------------------------------------
// multi-type unions: `type: [T1, T2, ...]` -> any
// ----------------------------------------------------------------------------

// A free-form map whose values are a multi-type union becomes
// map[string]any, so any of the declared JSON types round-trips.
func TestEventUnionValuedMap(t *testing.T) {
var e Event
require.NoError(t, json.Unmarshal([]byte(`{"name":"deploy","duration":1.5,"ok":true}`), &e))
assert.Equal(t, "deploy", e["name"])
assert.Equal(t, 1.5, e["duration"])
assert.Equal(t, true, e["ok"])
}

// Union-typed properties are plain `any` fields. Neither the required nor
// the optional one is pointer-wrapped, since nil is already `any`'s zero
// value. Compile-time check: each field takes a bare value.
func TestMeasurementUnionProperties(t *testing.T) {
m := Measurement{
Value: "high",
OptionalValue: 42,
NullableValue: nil,
}
data, err := json.Marshal(m)
require.NoError(t, err)
assert.JSONEq(t, `{"value":"high","optionalValue":42,"nullableValue":null}`, string(data))
}

// A named component that is itself a union is emitted as an alias
// (`type UnionValue = any`) rather than a newtype, so a value of any of the
// declared types is directly assignable. If the union were emitted as a
// newtype, the assignments below would fail to compile.
func TestUnionValueIsAlias(t *testing.T) {
var v UnionValue = "anything"
assert.Equal(t, "anything", v)

v = 3
assert.Equal(t, 3, v)
}

// A union carrying an `enum` must not generate typed constants: `const X any
// = ...` does not compile. The enum is dropped and the schema stays the plain
// `any` alias, so this file building at all is the assertion.
func TestUnionEnumDropsConstants(t *testing.T) {
var v UnionEnum = "two"
assert.Equal(t, "two", v)
}

// petFieldComments extracts the doc comment text for each field of the Pet
// struct from a parsed AST. Returns map[fieldName]commentText.
func petFieldComments(t *testing.T, f *ast.File) map[string]string {
Expand Down
47 changes: 47 additions & 0 deletions internal/test/openapi31/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,50 @@ components:
Status:
type: string
const: active

# ----------------------------------------------------------------------
# multi-type unions: `type: [T1, T2, ...]` -> any
# ----------------------------------------------------------------------
# 3.1 lets `type` hold a list, so a value may be any one of several types.
# Go has no type expressing that constraint, so each of these lowers to
# `any`, the same permissive mapping a typeless schema gets.
#
# A free-form map whose values are genuinely polymorphic scalars. This is
# the shape that appears in published 3.1 specs (e.g. an event payload
# whose fields may be strings, numbers or booleans). Expected Go shape:
# map[string]any.
Event:
type: object
additionalProperties:
type: [string, number, boolean]

# Unions in property position. `value` is required and `optionalValue` is
# not, but neither is pointer-wrapped: nil is already `any`'s zero value.
# `nullableValue` adds "null" to the list, which is stripped as the
# nullability marker before the union check, leaving a two-type union.
Measurement:
type: object
required:
- value
properties:
value:
type: [string, number]
optionalValue:
type: [string, number]
nullableValue:
type: [string, number, "null"]

# A named component that is itself a union. Emitted as a type alias
# (`type UnionValue = any`) rather than a newtype.
UnionValue:
type: [string, number, boolean]

# A union carrying an `enum` does not become a Go typed constant:
# `const X any = ...` is not a valid Go constant. The enum is dropped and
# the schema generates as the plain `any` the union maps to, the same way
# `type: array` with an `enum` falls through to plain array handling.
UnionEnum:
type: [string, integer]
enum:
- 1
- two
7 changes: 7 additions & 0 deletions internal/test/openapi31params/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=../../../configuration-schema.json
package: openapi31params
output: openapi31params.gen.go
generate:
models: true
std-http-server: true
client: true
17 changes: 17 additions & 0 deletions internal/test/openapi31params/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Package openapi31params verifies that OpenAPI 3.1 multi-type union
// parameters (`type: [string, integer]`) bind end-to-end. The models-only
// lowering of unions to `any` is covered in the sibling openapi31 suite;
// this suite generates a std-http server and client so the emitted
// bind-option Types lists are exercised against the real runtime binder
// (runtime >= v1.7.0), in path, query, and header positions:
//
// - the value binds to the first union member that parses, trying
// boolean, integer, number, then string — so "42" arrives as int64(42)
// and "abc" as "abc" in the same `any` parameter.
// - a "null" entry in the type list is the nullability marker, not a
// member, and is stripped from the emitted Types list.
// - single-type parameters emit no Types field at all, keeping their
// generated code identical to before.
package openapi31params

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml
Loading