diff --git a/reflect.go b/reflect.go index 163fb7f..5ba4ce2 100644 --- a/reflect.go +++ b/reflect.go @@ -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), } @@ -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. diff --git a/reflect_test.go b/reflect_test.go index 2a1dd4c..d502309 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -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]}`))) +}