Skip to content

Commit

Permalink
Merge pull request #612 from danielgtaylor/nullable-array-option
Browse files Browse the repository at this point in the history
fix: make nullable arrays configurable
  • Loading branch information
danielgtaylor authored Oct 17, 2024
2 parents 0f7ad56 + 7adb2c5 commit 1d5dce7
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 6 deletions.
15 changes: 10 additions & 5 deletions docs/docs/features/request-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ Huma tries to balance schema simplicity, usability, and broad compatibility with
Fields being nullable is determined automatically but can be overridden as needed using the logic below:

1. Start with no fields as nullable
2. If a field is a pointer:
2. If a field is a pointer (including slices):
1. To a `boolean`, `integer`, `number`, `string`: it is nullable unless it has `omitempty`.
2. To an `array`, `object`: it is **not** nullable, due to complexity and bad support for `anyOf`/`oneOf` in many tools.
2. To an `array`: it is nullable if `huma.DefaultArrayNullable` is true.
3. To an `object`: it is **not** nullable, due to complexity and bad support for `anyOf`/`oneOf` in many tools.
3. If a field has `nullable:"false"`, it is not nullable
4. If a field has `nullable:"true"`:
1. To a `boolean`, `integer`, `number`, `string`: it is nullable
2. To an `array`, `object`: **panic** saying this is not currently supported
1. To a `boolean`, `integer`, `number`, `string`, `array`: it is nullable
2. To an `object`: **panic** saying this is not currently supported
5. If a struct has a field `_` with `nullable: true`, the struct is nullable enabling users to opt-in for `object` without the `anyOf`/`oneOf` complication.

Here are some examples:
Expand All @@ -77,7 +78,7 @@ type MyStruct1 struct {
}

// Make a specific scalar field nullable. This is *not* supported for
// slices, maps, or structs. Structs *must* use the method above.
// maps or structs. Structs *must* use the method above.
type MyStruct2 struct {
Field1 *string `json:"field1"`
Field2 string `json:"field2" nullable:"true"`
Expand All @@ -86,6 +87,10 @@ type MyStruct2 struct {

Nullable types will generate a type array like `"type": ["string", "null"]` which has broad compatibility and is easy to downgrade to OpenAPI 3.0. Also keep in mind you can always provide a [custom schema](./schema-customization.md) if the built-in features aren't exactly what you need.

!!! info "Note"

Slices in Go marshal into JSON as `null` if the slice itself is `nil` rather than allocated but empty. This is why slices are nullable by default. See the [Go JSON package documentation](https://pkg.go.dev/encoding/json#Marshal) for more information.

## Validation Tags

The following additional tags are supported on model fields:
Expand Down
8 changes: 7 additions & 1 deletion schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import (
// ErrSchemaInvalid is sent when there is a problem building the schema.
var ErrSchemaInvalid = errors.New("schema is invalid")

// DefaultArrayNullable controls whether arrays are nullable by default. Set
// this to `false` to make arrays non-nullable by default, but be aware that
// any `nil` slice will still encode as `null` in JSON. See also:
// https://pkg.go.dev/encoding/json#Marshal.
var DefaultArrayNullable = true

// JSON Schema type constants
const (
TypeBoolean = "boolean"
Expand Down Expand Up @@ -763,7 +769,7 @@ func schemaFromType(r Registry, t reflect.Type) *Schema {
s.ContentEncoding = "base64"
} else {
s.Type = TypeArray
s.Nullable = true
s.Nullable = DefaultArrayNullable
s.Items = r.Schema(t.Elem(), true, t.Name()+"Item")

if t.Kind() == reflect.Array {
Expand Down
56 changes: 56 additions & 0 deletions schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,46 @@ func TestSchema(t *testing.T) {
"required": ["int"]
}`,
},
{
name: "field-nullable-array",
input: struct {
Int []int64 `json:"int" nullable:"true"`
}{},
expected: `{
"type": "object",
"additionalProperties": false,
"properties": {
"int": {
"type": ["array", "null"],
"items": {
"type": "integer",
"format": "int64"
}
}
},
"required": ["int"]
}`,
},
{
name: "field-non-nullable-array",
input: struct {
Int []int64 `json:"int" nullable:"false"`
}{},
expected: `{
"type": "object",
"additionalProperties": false,
"properties": {
"int": {
"type": "array",
"items": {
"type": "integer",
"format": "int64"
}
}
},
"required": ["int"]
}`,
},
{
name: "field-nullable-struct",
input: struct {
Expand Down Expand Up @@ -1319,6 +1359,22 @@ func TestMarshalDiscriminator(t *testing.T) {
}`, string(b))
}

func TestSchemaArrayNotNullable(t *testing.T) {
huma.DefaultArrayNullable = false
defer func() {
huma.DefaultArrayNullable = true
}()

type Value struct {
Field []string `json:"field"`
}

r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer)
s := r.Schema(reflect.TypeOf(Value{}), false, "")

assert.Equal(t, "array", s.Properties["field"].Type)
}

type BenchSub struct {
Visible bool `json:"visible" default:"true"`
Metrics []float64 `json:"metrics" maxItems:"31"`
Expand Down

0 comments on commit 1d5dce7

Please sign in to comment.