Skip to content
Draft
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
7 changes: 6 additions & 1 deletion pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +70 to 72
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 Mapped null parameters break

The new null branch makes .RequiresNilCheck true for every null parameter, even though type-mapping.null can map null to a non-nilable Go type. With a mapping like null to string, client, webhook, and callback templates can generate if params.X != nil for a string parameter field. That generated code does not compile. Parameter nil checks should follow the resolved Go type instead of the OpenAPI type alone.


Expand Down
80 changes: 80 additions & 0 deletions pkg/codegen/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
}
})
}
}
11 changes: 10 additions & 1 deletion pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +198 to 200
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 Mapped null may not be nilable

type-mapping.null is configurable, but this helper now treats every null schema as having a nil zero value. If a user maps null to a non-nilable Go type such as string, union and additional-properties marshal templates can emit checks like if t.Field != nil against that non-nilable field. The generated model code then fails to compile. This check needs to be based on the resolved Go type, or null mappings need to be constrained to nilable types.


Expand Down Expand Up @@ -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
Comment on lines +1015 to +1018
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 Null enums break generation

This branch lets type: "null" schemas reach the enum/const generation path, but null enum values are later stringified as <nil> and emitted as unquoted Go constants. For an OpenAPI 3.1 schema with type: "null" and enum: [null] or const: null, the generated code contains an invalid assignment like a null enum constant set to <nil>, so the output does not compile. This path should either render a valid nil expression for the chosen representation or reject null enums before constants are emitted.

} else {
return fmt.Errorf("unhandled Schema type: %v", t)
}
Expand Down
49 changes: 49 additions & 0 deletions pkg/codegen/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
})
}
}
5 changes: 5 additions & 0 deletions pkg/codegen/typemapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -104,4 +106,7 @@ var DefaultTypeMapping = TypeMapping{
"binary": {Type: "openapi_types.File"},
},
},
Null: FormatMapping{
Default: SimpleTypeSpec{Type: "interface{}"},
},
}
6 changes: 6 additions & 0 deletions pkg/codegen/typemapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}