Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert nullable schema generation for array slices #594

Closed
wants to merge 2 commits into from
Closed
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
4 changes: 4 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ type Config struct {
CreateHooks []func(Config) Config
}

func (c *Config) RegistryConfig() *RegistryConfig {
return c.Components.Schemas.Config()
}

// API represents a Huma API wrapping a specific router.
type API interface {
// Adapter returns the router adapter for this API, providing a generic
Expand Down
10 changes: 10 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,13 @@ func TestChiRouterPrefix(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "/api/openapi.yaml")
}

func TestRegistryConfig(t *testing.T) {
cfg := huma.DefaultConfig("Test", "3.1")

// Test default value
assert.False(t, cfg.Components.Schemas.Config().NullSlices)

cfg.RegistryConfig().NullSlices = true
assert.True(t, cfg.Components.Schemas.Config().NullSlices)
}
18 changes: 18 additions & 0 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import (
"unicode/utf8"
)

// RegistryConfig contains configuration options to control how OpenAPI schemas
// are generated
type RegistryConfig struct {
// NullSlices specifies whether slices in request and response body should be
// marked as nullable in the generated OpenAPI spec to reflect the behavior
// of [encoding/json] package.
NullSlices bool
}

// Registry creates and stores schemas and their references, and supports
// marshalling to JSON/YAML for use as an OpenAPI #/components/schemas object.
// Behavior is implementation-dependent, but the design allows for recursive
Expand All @@ -20,6 +29,7 @@ type Registry interface {
TypeFromRef(ref string) reflect.Type
Map() map[string]*Schema
RegisterTypeAlias(t reflect.Type, alias reflect.Type)
Config() *RegistryConfig
}

// DefaultSchemaNamer provides schema names for types. It uses the type name
Expand Down Expand Up @@ -66,6 +76,11 @@ type mapRegistry struct {
seen map[reflect.Type]bool
namer func(reflect.Type, string) string
aliases map[reflect.Type]reflect.Type
config RegistryConfig
}

func (r *mapRegistry) Config() *RegistryConfig {
return &r.config
}

func (r *mapRegistry) Schema(t reflect.Type, allowRef bool, hint string) *Schema {
Expand Down Expand Up @@ -170,5 +185,8 @@ func NewMapRegistry(prefix string, namer func(t reflect.Type, hint string) strin
seen: map[reflect.Type]bool{},
aliases: map[reflect.Type]reflect.Type{},
namer: namer,
config: RegistryConfig{
NullSlices: false,
},
}
}
2 changes: 1 addition & 1 deletion schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,7 @@ func schemaFromType(r Registry, t reflect.Type) *Schema {
s.ContentEncoding = "base64"
} else {
s.Type = TypeArray
s.Nullable = true
s.Nullable = r.Config().NullSlices
s.Items = r.Schema(t.Elem(), true, t.Name()+"Item")

if t.Kind() == reflect.Array {
Expand Down
62 changes: 51 additions & 11 deletions schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,16 @@ func (c *TypedIntegerWithCustomLimits) TransformSchema(r huma.Registry, s *huma.
func TestSchema(t *testing.T) {
bitSize := strconv.Itoa(bits.UintSize)

nullSlicesCfg := huma.RegistryConfig{
NullSlices: true,
}

cases := []struct {
name string
input any
expected string
panics string
config huma.RegistryConfig
}{
{
name: "bool",
Expand Down Expand Up @@ -205,12 +210,12 @@ func TestSchema(t *testing.T) {
{
name: "array",
input: [2]int{1, 2},
expected: `{"type": ["array", "null"], "items": {"type": "integer", "format": "int64"}, "minItems": 2, "maxItems": 2}`,
expected: `{"type": "array", "items": {"type": "integer", "format": "int64"}, "minItems": 2, "maxItems": 2}`,
},
{
name: "slice",
input: []int{1, 2, 3},
expected: `{"type": ["array", "null"], "items": {"type": "integer", "format": "int64"}}`,
expected: `{"type": "array", "items": {"type": "integer", "format": "int64"}}`,
},
{
name: "map",
Expand Down Expand Up @@ -286,7 +291,7 @@ func TestSchema(t *testing.T) {
"type": "object",
"properties": {
"value": {
"type": ["array", "null"],
"type": "array",
"minItems": 1,
"maxItems": 10,
"uniqueItems": true,
Expand Down Expand Up @@ -344,7 +349,7 @@ func TestSchema(t *testing.T) {
"type": "object",
"properties": {
"value": {
"type": ["array", "null"],
"type": "array",
"items": {
"type": "integer",
"format": "int64",
Expand Down Expand Up @@ -434,7 +439,7 @@ func TestSchema(t *testing.T) {
"type": "object",
"properties": {
"value": {
"type": ["array", "null"],
"type": "array",
"items": {
"type": "string"
},
Expand All @@ -454,7 +459,7 @@ func TestSchema(t *testing.T) {
"type": "object",
"properties": {
"value": {
"type": ["array", "null"],
"type": "array",
"items": {
"type": "integer",
"format": "int64"
Expand Down Expand Up @@ -753,7 +758,7 @@ func TestSchema(t *testing.T) {
},
"maxItems":1,
"minItems":1,
"type":["array", "null"]
"type":"array"
},
"byRef":{
"$ref":"#/components/schemas/RecursiveChildKey"
Expand All @@ -771,7 +776,7 @@ func TestSchema(t *testing.T) {
"items":{
"$ref":"#/components/schemas/RecursiveChildLoop"
},
"type":["array", "null"]}
"type":"array"}
},
"required":["slice","array","map","byValue", "byRef"],
"type":"object"
Expand Down Expand Up @@ -893,7 +898,7 @@ func TestSchema(t *testing.T) {
"additionalProperties":false,
"properties":{
"values":{
"type":["array", "null"],
"type":"array",
"items":{
"type":"string",
"minLength":1,
Expand Down Expand Up @@ -1050,7 +1055,7 @@ func TestSchema(t *testing.T) {
},
"maxItems":4,
"minItems":4,
"type":["array", "null"]
"type":"array"
}
},
"required":["value"],
Expand All @@ -1073,7 +1078,7 @@ func TestSchema(t *testing.T) {
},
"maxItems":4,
"minItems":4,
"type":["array", "null"]
"type":"array"
}
},
"required":["value"],
Expand All @@ -1095,12 +1100,47 @@ func TestSchema(t *testing.T) {
"type":"object"
}`,
},
{
name: "array-null-cfg",
config: nullSlicesCfg,
input: [2]int{1, 2},
expected: `{"type": ["array", "null"], "items": {"type": "integer", "format": "int64"}, "minItems": 2, "maxItems": 2}`,
},
{
name: "slice-null-cfg",
config: nullSlicesCfg,
input: []int{1, 2, 3},
expected: `{"type": ["array", "null"], "items": {"type": "integer", "format": "int64"}}`,
},
{
name: "field-array-null-cfg",
config: nullSlicesCfg,
input: struct {
Value []int `json:"value" minItems:"1" maxItems:"10" uniqueItems:"true"`
}{},
expected: `{
"type": "object",
"properties": {
"value": {
"type": ["array", "null"],
"minItems": 1,
"maxItems": 10,
"uniqueItems": true,
"items": {"type": "integer", "format": "int64"}
}
},
"required": ["value"],
"additionalProperties": false
}`,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer)

r.Config().NullSlices = c.config.NullSlices

if c.panics != "" {
assert.PanicsWithError(t, c.panics, func() {
r.Schema(reflect.TypeOf(c.input), false, "")
Expand Down