Skip to content
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
37 changes: 36 additions & 1 deletion reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,9 @@ func (t *Schema) UnmarshalJSON(data []byte) error {
type SchemaAlt Schema
aux := &struct {
*SchemaAlt
// Shadows Schema.Type to accept both the string form ("string") and the
// array-union form (["string", "null"]) of the JSON Schema type keyword.
Type json.RawMessage `json:"type,omitempty"`
}{
SchemaAlt: (*SchemaAlt)(t),
}
Expand All @@ -1172,7 +1175,39 @@ func (t *Schema) UnmarshalJSON(data []byte) error {
}
}

return json.Unmarshal(data, aux)
if err := json.Unmarshal(data, aux); err != nil {
return err
}
return t.unmarshalType(aux.Type)
}

// unmarshalType assigns the type keyword: a string assigns Type directly; an array
// (type union, e.g. ["string", "null"]) is normalized to anyOf branches — merged as
// an allOf member when the schema already carries its own anyOf, so both apply.
func (t *Schema) unmarshalType(raw json.RawMessage) error {
if len(raw) == 0 {
return nil
}

if raw[0] == '"' {
return json.Unmarshal(raw, &t.Type)
}

var types []string
if err := json.Unmarshal(raw, &types); err != nil {
return err
}

branches := make([]*Schema, 0, len(types))
for _, typ := range types {
branches = append(branches, &Schema{Type: typ})
}
if len(t.AnyOf) == 0 {
t.AnyOf = branches
} else {
t.AllOf = append(t.AllOf, &Schema{AnyOf: branches})
}
return nil
}

// MarshalJSON is used to serialize a schema object or boolean.
Expand Down
53 changes: 53 additions & 0 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,3 +715,56 @@ func TestJSONSchemaByValue(t *testing.T) {
compareSchemaOutput(t, "fixtures/schema_by_value.json", r, val)
compareSchemaOutput(t, "fixtures/schema_by_value_pointer.json", r, &val)
}

func TestUnmarshalTypeArray(t *testing.T) {
raw := []byte(`{
"type": "object",
"properties": {
"expiry": {
"type": ["string", "null"],
"format": "date",
"description": "optional expiry date"
},
"name": {"type": "string"}
}
}`)

schema := new(Schema)
require.NoError(t, schema.UnmarshalJSON(raw))
assert.Equal(t, "object", schema.Type)

expiry, ok := schema.Properties.Get("expiry")
require.True(t, ok)
assert.Empty(t, expiry.Type)
require.Len(t, expiry.AnyOf, 2)
assert.Equal(t, "string", expiry.AnyOf[0].Type)
assert.Equal(t, "null", expiry.AnyOf[1].Type)
assert.Equal(t, "date", expiry.Format)
assert.Equal(t, "optional expiry date", expiry.Description)

name, ok := schema.Properties.Get("name")
require.True(t, ok)
assert.Equal(t, "string", name.Type)
}

func TestUnmarshalTypeArrayWithExistingAnyOf(t *testing.T) {
raw := []byte(`{
"type": ["integer", "null"],
"anyOf": [{"minimum": 0}, {"maximum": -10}]
}`)

schema := new(Schema)
require.NoError(t, schema.UnmarshalJSON(raw))
assert.Empty(t, schema.Type)
require.Len(t, schema.AnyOf, 2)
require.Len(t, schema.AllOf, 1)
require.Len(t, schema.AllOf[0].AnyOf, 2)
assert.Equal(t, "integer", schema.AllOf[0].AnyOf[0].Type)
assert.Equal(t, "null", schema.AllOf[0].AnyOf[1].Type)
}

func TestUnmarshalTypeInvalid(t *testing.T) {
schema := new(Schema)
require.Error(t, schema.UnmarshalJSON([]byte(`{"type": 5}`)))
require.Error(t, schema.UnmarshalJSON([]byte(`{"type": [5]}`)))
}