Skip to content
Open
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
47 changes: 46 additions & 1 deletion field_parser_v3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@ func TestDefaultFieldParserV3(t *testing.T) {
}},
).ComplementSchema(schema)
assert.NoError(t, err)
assert.Equal(t, true, schema.Spec.Extensions["x-nullable"])
// A truthy x-nullable is expressed as the standard type union instead
// of a vendor extension.
assert.Equal(t, &spec.SingleOrArray[string]{INTEGER, NULL}, schema.Spec.Type)
_, hasNullableExt := schema.Spec.Extensions["x-nullable"]
assert.False(t, hasNullableExt)
assert.Equal(t, "def", schema.Spec.Extensions["x-abc"])
assert.Equal(t, false, schema.Spec.Extensions["x-omitempty"])
assert.Equal(t, "[0, 9]", schema.Spec.Extensions["x-example"])
Expand Down Expand Up @@ -864,3 +868,44 @@ func TestValidTagsV3(t *testing.T) {
assert.Equal(t, "^[a-zA-Z0-9_]*$", schema.Spec.Pattern)
})
}

func TestComplementSchemaNullableExtensionV3(t *testing.T) {
t.Parallel()

complement := func(tag string) *spec.RefOrSpec[spec.Schema] {
schema := spec.NewSchemaSpec()
schema.Spec.Type = &spec.SingleOrArray[string]{STRING}
err := newTagBaseFieldParserV3(
&Parser{},
&ast.File{Name: &ast.Ident{Name: "test"}},
&ast.Field{
Names: []*ast.Ident{{Name: "Field"}},
Tag: &ast.BasicLit{Value: tag},
},
).ComplementSchema(schema)
assert.NoError(t, err)
return schema
}

t.Run("x-nullable becomes a type union", func(t *testing.T) {
t.Parallel()
schema := complement(`json:"test" extensions:"x-nullable"`)
assert.Equal(t, &spec.SingleOrArray[string]{STRING, NULL}, schema.Spec.Type)
_, hasExt := schema.Spec.Extensions["x-nullable"]
assert.False(t, hasExt)
})

t.Run("negated x-nullable stays an extension", func(t *testing.T) {
t.Parallel()
schema := complement(`json:"test" extensions:"!x-nullable"`)
assert.Equal(t, &spec.SingleOrArray[string]{STRING}, schema.Spec.Type)
assert.Equal(t, false, schema.Spec.Extensions["x-nullable"])
})

t.Run("other extensions are preserved", func(t *testing.T) {
t.Parallel()
schema := complement(`json:"test" extensions:"x-nullable,x-abc=def"`)
assert.Equal(t, &spec.SingleOrArray[string]{STRING, NULL}, schema.Spec.Type)
assert.Equal(t, "def", schema.Spec.Extensions["x-abc"])
})
}
11 changes: 11 additions & 0 deletions field_parserv3.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,17 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st
extensionsTagValue := ps.tag.Get(extensionsTag)
if extensionsTagValue != "" {
schema.Extensions = setExtensionParam(extensionsTagValue)

// OpenAPI 3.1 has no `nullable` keyword and ignores vendor
// extensions, so express extensions:"x-nullable" as the standard
// type union [T, "null"] instead.
if nullable, ok := schema.Extensions["x-nullable"]; ok {
if isNullable, isBool := nullable.(bool); isBool && isNullable && schema.Type != nil && len(*schema.Type) > 0 {
types := append(*schema.Type, NULL)
schema.Type = &types
delete(schema.Extensions, "x-nullable")
}
}
}

varNamesTag := ps.tag.Get("x-enum-varnames")
Expand Down
2 changes: 2 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
const (
// ARRAY represent a array value.
ARRAY = "array"
// NULL represent a null type in an OpenAPI 3.1 type union.
NULL = "null"
// OBJECT represent a object value.
OBJECT = "object"
// PRIMITIVE represent a primitive value.
Expand Down