From 25c0765d08ab54f0c89e1808baa8db33cac99ea8 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Mon, 6 Jul 2026 11:28:49 -0700 Subject: [PATCH] Handle pure nullable types Closes: #2430 OpenAPI 3.1 allows a schema whose only type is "null", which validates exactly the JSON value null. The type dispatch in oapiSchemaToGoType had no branch for it, so codegen failed with "unhandled Schema type: &[null]". Map such schemas to `any`, mirroring the permissive mapping used for typeless schemas, and skip the optional pointer since nil is already the zero value of `any`. Includes a regression test under internal/test/issues/issue-2430. Co-Authored-By: Claude Fable 5 --- internal/test/issues/issue-2430/config.yaml | 8 ++++ internal/test/issues/issue-2430/generate.go | 3 ++ .../test/issues/issue-2430/issue2430.gen.go | 19 ++++++++++ .../test/issues/issue-2430/issue2430_test.go | 37 +++++++++++++++++++ internal/test/issues/issue-2430/spec.yaml | 33 +++++++++++++++++ pkg/codegen/schema.go | 12 ++++++ pkg/codegen/schema_test.go | 12 ++++++ 7 files changed, 124 insertions(+) create mode 100644 internal/test/issues/issue-2430/config.yaml create mode 100644 internal/test/issues/issue-2430/generate.go create mode 100644 internal/test/issues/issue-2430/issue2430.gen.go create mode 100644 internal/test/issues/issue-2430/issue2430_test.go create mode 100644 internal/test/issues/issue-2430/spec.yaml diff --git a/internal/test/issues/issue-2430/config.yaml b/internal/test/issues/issue-2430/config.yaml new file mode 100644 index 0000000000..0f11f5eaae --- /dev/null +++ b/internal/test/issues/issue-2430/config.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=../../../../configuration-schema.json +package: issue2430 +output: issue2430.gen.go +generate: + models: true +output-options: + skip-prune: true diff --git a/internal/test/issues/issue-2430/generate.go b/internal/test/issues/issue-2430/generate.go new file mode 100644 index 0000000000..2b4383faec --- /dev/null +++ b/internal/test/issues/issue-2430/generate.go @@ -0,0 +1,3 @@ +package issue2430 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-2430/issue2430.gen.go b/internal/test/issues/issue-2430/issue2430.gen.go new file mode 100644 index 0000000000..7dec565154 --- /dev/null +++ b/internal/test/issues/issue-2430/issue2430.gen.go @@ -0,0 +1,19 @@ +// Package issue2430 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue2430 + +// ChallengeOpenJson defines model for ChallengeOpenJson. +type ChallengeOpenJson struct { + Challenger any `json:"challenger,omitempty"` + Id string `json:"id"` + Url *string `json:"url,omitempty"` +} + +// NullOnly defines model for NullOnly. +type NullOnly = any + +// RequiredNull defines model for RequiredNull. +type RequiredNull struct { + Value any `json:"value"` +} diff --git a/internal/test/issues/issue-2430/issue2430_test.go b/internal/test/issues/issue-2430/issue2430_test.go new file mode 100644 index 0000000000..9d99474703 --- /dev/null +++ b/internal/test/issues/issue-2430/issue2430_test.go @@ -0,0 +1,37 @@ +package issue2430 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Regression test for https://github.com/oapi-codegen/oapi-codegen/issues/2430. +// +// OpenAPI 3.1 allows a schema whose only type is "null" (as opposed to a +// type array that merely includes "null"). Such a schema validates exactly +// the JSON value null. The generator used to fail with "unhandled Schema +// type: &[null]"; it now maps the schema to `any`, with no optional +// pointer, since nil is already `any`'s zero value. + +func TestNullTypePropertyRoundTrip(t *testing.T) { + var v ChallengeOpenJson + require.NoError(t, json.Unmarshal([]byte(`{"id":"1","challenger":null}`), &v)) + assert.Equal(t, "1", v.Id) + assert.Nil(t, v.Challenger) + + out, err := json.Marshal(RequiredNull{}) + require.NoError(t, err) + // The required null-typed field must serialize as an explicit null. + assert.JSONEq(t, `{"value":null}`, string(out)) +} + +func TestNullTypeComponentIsAny(t *testing.T) { + // NullOnly is an alias for `any`; assigning an arbitrary value must + // compile, and nil round-trips through JSON as null. + var n NullOnly + require.NoError(t, json.Unmarshal([]byte(`null`), &n)) + assert.Nil(t, n) +} diff --git a/internal/test/issues/issue-2430/spec.yaml b/internal/test/issues/issue-2430/spec.yaml new file mode 100644 index 0000000000..009fc75635 --- /dev/null +++ b/internal/test/issues/issue-2430/spec.yaml @@ -0,0 +1,33 @@ +openapi: 3.1.0 +info: + title: issue-2430 + version: 1.0.0 +paths: {} +components: + schemas: + # A property whose schema is the bare OpenAPI 3.1 null type: the only + # valid JSON value for `challenger` is null. Maps to `any` in Go. + ChallengeOpenJson: + type: object + required: + - id + properties: + id: + type: string + url: + type: string + format: uri + challenger: + type: "null" + # A required null-typed property: must not be wrapped in a pointer, + # since `any` already has nil as its zero value. + RequiredNull: + type: object + required: + - value + properties: + value: + type: "null" + # A named component that is itself the bare null type. + NullOnly: + type: "null" diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index c109e54a58..0081fa41a4 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -1186,6 +1186,18 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem outSchema.SkipOptionalPointer = true } outSchema.DefineViaAlias = true + } else if t.Is("null") { + // OpenAPI 3.1 allows a schema whose only type is "null", which + // validates exactly the JSON value null. Go has no type whose only + // value is null, so map it to `any` — the same permissive mapping + // used for typeless schemas — rather than rejecting an otherwise + // valid spec. An optional pointer makes no sense for `any`, whose + // zero value is already nil, so skip it — mirroring the + // typeless-schema handling in GenerateGoSchema. + // See https://github.com/oapi-codegen/oapi-codegen/issues/2430 + outSchema.GoType = "any" + outSchema.SkipOptionalPointer = true + outSchema.DefineViaAlias = true } else { return fmt.Errorf("unhandled Schema type: %v", t) } diff --git a/pkg/codegen/schema_test.go b/pkg/codegen/schema_test.go index 77c1a8751a..fafeeaca5f 100644 --- a/pkg/codegen/schema_test.go +++ b/pkg/codegen/schema_test.go @@ -527,3 +527,15 @@ func TestProperty_ZeroValueIsNil(t *testing.T) { }) } } + +// A bare OpenAPI 3.1 `type: "null"` schema validates exactly the JSON +// value null. Go has no such type, so it maps to `any`, with the optional +// pointer skipped (nil is already `any`'s zero value). Issue #2430. +func TestOapiSchemaToGoType_NullType(t *testing.T) { + schema := &openapi3.Schema{Type: &openapi3.Types{"null"}} + var out Schema + require.NoError(t, oapiSchemaToGoType(schema, []string{"Challenger"}, &out)) + assert.Equal(t, "any", out.GoType) + assert.True(t, out.SkipOptionalPointer) + assert.True(t, out.DefineViaAlias) +}