diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 7fde7f0a6..97b43efab 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -62,7 +62,12 @@ func (pd ParameterDefinition) ZeroValueIsNil() bool { return false } - if schemaPrimaryType(pd.Schema.OAPISchema.Type).Is("array") { + t := schemaPrimaryType(pd.Schema.OAPISchema.Type) + if t.Is("array") { + return true + } + + if t.Is("null") { return true } diff --git a/pkg/codegen/operations_test.go b/pkg/codegen/operations_test.go index fae988bbf..6cb932057 100644 --- a/pkg/codegen/operations_test.go +++ b/pkg/codegen/operations_test.go @@ -19,6 +19,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsJson(t *testing.T) { @@ -228,3 +229,82 @@ func TestJsonTag(t *testing.T) { assert.Equal(t, "`db:\"foo_col\" json:\"foo\" validate:\"param-level\"`", pd.JsonTag()) }) } + +func TestParameterDefinition_ZeroValueIsNil(t *testing.T) { + newType := func(typ string) *openapi3.Types { + return &openapi3.Types{typ} + } + + tests := []struct { + name string + oapiSchema *openapi3.Schema + goType string + expectIsNil bool + }{ + { + name: "when an array, returns true", + oapiSchema: &openapi3.Schema{Type: newType("array")}, + expectIsNil: true, + }, + { + name: "when an object, returns false", + oapiSchema: &openapi3.Schema{Type: newType("object")}, + expectIsNil: false, + }, + { + name: "when an object rendered as a map, returns true", + oapiSchema: &openapi3.Schema{Type: newType("object")}, + goType: "map[string]string", + expectIsNil: true, + }, + { + name: "when a string, returns false", + oapiSchema: &openapi3.Schema{Type: newType("string")}, + expectIsNil: false, + }, + { + name: "when an integer, returns false", + oapiSchema: &openapi3.Schema{Type: newType("integer")}, + expectIsNil: false, + }, + { + name: "when a number, returns false", + oapiSchema: &openapi3.Schema{Type: newType("number")}, + expectIsNil: false, + }, + { + name: "when OAPISchema is nil, returns false", + oapiSchema: nil, + expectIsNil: false, + }, + { + name: "when OAPISchema is zero value, returns false", + oapiSchema: &openapi3.Schema{}, + expectIsNil: false, + }, + { + name: "when type is null, returns true", + oapiSchema: &openapi3.Schema{Type: newType("null")}, + expectIsNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pd := ParameterDefinition{ + Spec: &openapi3.Parameter{ + Schema: &openapi3.SchemaRef{Value: tt.oapiSchema}, + }, + Schema: Schema{ + OAPISchema: tt.oapiSchema, + GoType: tt.goType, + }, + } + if tt.expectIsNil { + require.True(t, pd.ZeroValueIsNil()) + } else { + require.False(t, pd.ZeroValueIsNil()) + } + }) + } +} diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index 48a695b2e..97f2447ea 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -190,7 +190,12 @@ func (p Property) ZeroValueIsNil() bool { return false } - if schemaPrimaryType(p.Schema.OAPISchema.Type).Is("array") { + t := schemaPrimaryType(p.Schema.OAPISchema.Type) + if t.Is("array") { + return true + } + + if t.Is("null") { return true } @@ -1007,6 +1012,10 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem outSchema.SkipOptionalPointer = true } outSchema.DefineViaAlias = true + } else if t.Is("null") { + spec := globalState.typeMapping.Null.Resolve(f) + outSchema.GoType = spec.Type + 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 77c1a8751..d0e6b231c 100644 --- a/pkg/codegen/schema_test.go +++ b/pkg/codegen/schema_test.go @@ -509,6 +509,11 @@ func TestProperty_ZeroValueIsNil(t *testing.T) { oapiSchema: &openapi3.Schema{}, expectIsNil: false, }, + { + name: "when type is null, returns true", + oapiSchema: &openapi3.Schema{Type: newType("null")}, + expectIsNil: true, + }, } for _, tt := range tests { @@ -527,3 +532,47 @@ func TestProperty_ZeroValueIsNil(t *testing.T) { }) } } + +func TestNullTypeSupport(t *testing.T) { + // Test that type: "null" maps to interface{} in Go + newType := func(typ string) *openapi3.Types { + return &openapi3.Types{typ} + } + + tests := []struct { + name string + schema *openapi3.Schema + expectedGo string + }{ + { + name: "type: null maps to interface{}", + schema: &openapi3.Schema{Type: newType("null")}, + expectedGo: "interface{}", + }, + { + name: "type: string maps to string", + schema: &openapi3.Schema{Type: newType("string")}, + expectedGo: "string", + }, + { + name: "type: integer maps to int", + schema: &openapi3.Schema{Type: newType("integer")}, + expectedGo: "int", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up global state for OpenAPI 3.1 + oldState := globalState + globalState.is31 = true + globalState.typeMapping = DefaultTypeMapping + defer func() { globalState = oldState }() + + schemaRef := &openapi3.SchemaRef{Value: tt.schema} + result, err := GenerateGoSchema(schemaRef, []string{"test"}) + require.NoError(t, err) + assert.Equal(t, tt.expectedGo, result.GoType) + }) + } +} diff --git a/pkg/codegen/typemapping.go b/pkg/codegen/typemapping.go index 2a54695f6..6e386c613 100644 --- a/pkg/codegen/typemapping.go +++ b/pkg/codegen/typemapping.go @@ -22,6 +22,7 @@ type TypeMapping struct { Number FormatMapping `yaml:"number,omitempty" json:"number"` Boolean FormatMapping `yaml:"boolean,omitempty" json:"boolean"` String FormatMapping `yaml:"string,omitempty" json:"string"` + Null FormatMapping `yaml:"null,omitempty" json:"null"` } // Merge returns a new TypeMapping with user overrides applied on top of base. @@ -31,6 +32,7 @@ func (base TypeMapping) Merge(user TypeMapping) TypeMapping { Number: base.Number.merge(user.Number), Boolean: base.Boolean.merge(user.Boolean), String: base.String.merge(user.String), + Null: base.Null.merge(user.Null), } } @@ -104,4 +106,7 @@ var DefaultTypeMapping = TypeMapping{ "binary": {Type: "openapi_types.File"}, }, }, + Null: FormatMapping{ + Default: SimpleTypeSpec{Type: "interface{}"}, + }, } diff --git a/pkg/codegen/typemapping_test.go b/pkg/codegen/typemapping_test.go index ca593cfd5..3c3f328ce 100644 --- a/pkg/codegen/typemapping_test.go +++ b/pkg/codegen/typemapping_test.go @@ -53,6 +53,9 @@ func TestTypeMapping_Merge(t *testing.T) { // Number and Boolean unchanged assert.Equal(t, "float32", merged.Number.Default.Type) assert.Equal(t, "bool", merged.Boolean.Default.Type) + + // Null unchanged (no user override) + assert.Equal(t, "interface{}", merged.Null.Default.Type) } func TestDefaultTypeMapping_Completeness(t *testing.T) { @@ -85,4 +88,7 @@ func TestDefaultTypeMapping_Completeness(t *testing.T) { assert.Equal(t, "openapi_types.UUID", dm.String.Resolve("uuid").Type) assert.Equal(t, "openapi_types.File", dm.String.Resolve("binary").Type) assert.Equal(t, "string", dm.String.Resolve("unknown").Type) + + // Null + assert.Equal(t, "interface{}", dm.Null.Resolve("").Type) }