Skip to content

Commit 70332c1

Browse files
author
Stefan Radu Popescu
committed
Adds support for multi-types #134
1 parent a446707 commit 70332c1

File tree

4 files changed

+120
-52
lines changed

4 files changed

+120
-52
lines changed

fixtures/multi_type_test.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://github.com/invopop/jsonschema/multi-type-test",
4+
"$ref": "#/$defs/MultiTypeTest",
5+
"$defs": {
6+
"MultiTypeTest": {
7+
"properties": {
8+
"Value": {
9+
"type": [
10+
"number",
11+
"object"
12+
]
13+
}
14+
},
15+
"additionalProperties": false,
16+
"type": "object",
17+
"required": [
18+
"Value"
19+
]
20+
}
21+
}
22+
}

reflect.go

Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package jsonschema
99
import (
1010
"bytes"
1111
"encoding/json"
12+
"fmt"
1213
"net"
1314
"net/url"
1415
"reflect"
@@ -295,8 +296,8 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
295296
// It will unmarshal either.
296297
if t.Implements(protoEnumType) {
297298
st.OneOf = []*Schema{
298-
{Type: "string"},
299-
{Type: "integer"},
299+
{Type: &Type{Types: []string{"string"}}},
300+
{Type: &Type{Types: []string{"integer"}}},
300301
}
301302
return st
302303
}
@@ -306,7 +307,7 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
306307
// TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7
307308
if t == ipType {
308309
// TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5
309-
st.Type = "string"
310+
st.Type = &Type{Types: []string{"string"}}
310311
st.Format = "ipv4"
311312
return st
312313
}
@@ -326,16 +327,16 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
326327

327328
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
328329
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
329-
st.Type = "integer"
330+
st.Type = &Type{Types: []string{"integer"}}
330331

331332
case reflect.Float32, reflect.Float64:
332-
st.Type = "number"
333+
st.Type = &Type{Types: []string{"number"}}
333334

334335
case reflect.Bool:
335-
st.Type = "boolean"
336+
st.Type = &Type{Types: []string{"boolean"}}
336337

337338
case reflect.String:
338-
st.Type = "string"
339+
st.Type = &Type{Types: []string{"string"}}
339340

340341
default:
341342
panic("unsupported type " + t.String())
@@ -400,19 +401,19 @@ func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type,
400401
st.MaxItems = &l
401402
}
402403
if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() {
403-
st.Type = "string"
404+
st.Type = &Type{Types: []string{"string"}}
404405
// NOTE: ContentMediaType is not set here
405406
st.ContentEncoding = "base64"
406407
} else {
407-
st.Type = "array"
408+
st.Type = &Type{Types: []string{"array"}}
408409
st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem())
409410
}
410411
}
411412

412413
func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) {
413414
r.addDefinition(definitions, t, st)
414415

415-
st.Type = "object"
416+
st.Type = &Type{Types: []string{"object"}}
416417
if st.Description == "" {
417418
st.Description = r.lookupComment(t, "")
418419
}
@@ -435,17 +436,17 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Sc
435436
// Handle special types
436437
switch t {
437438
case timeType: // date-time RFC section 7.3.1
438-
s.Type = "string"
439+
s.Type = &Type{Types: []string{"string"}}
439440
s.Format = "date-time"
440441
return
441442
case uriType: // uri RFC section 7.3.6
442-
s.Type = "string"
443+
s.Type = &Type{Types: []string{"string"}}
443444
s.Format = "uri"
444445
return
445446
}
446447

447448
r.addDefinition(definitions, t, s)
448-
s.Type = "object"
449+
s.Type = &Type{Types: []string{"object"}}
449450
s.Properties = NewProperties()
450451
s.Description = r.lookupComment(t, "")
451452
if r.AssignAnchor {
@@ -524,7 +525,7 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r
524525
OneOf: []*Schema{
525526
property,
526527
{
527-
Type: "null",
528+
Type: &Type{Types: []string{"null"}},
528529
},
529530
},
530531
}
@@ -614,18 +615,23 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p
614615
tags := splitOnUnescapedCommas(f.Tag.Get("jsonschema"))
615616
tags = t.genericKeywords(tags, parent, propertyName)
616617

617-
switch t.Type {
618-
case "string":
619-
t.stringKeywords(tags)
620-
case "number":
621-
t.numericalKeywords(tags)
622-
case "integer":
623-
t.numericalKeywords(tags)
624-
case "array":
625-
t.arrayKeywords(tags)
626-
case "boolean":
627-
t.booleanKeywords(tags)
618+
if t.Type != nil {
619+
for _, currType := range t.Type.Types {
620+
switch currType {
621+
case "string":
622+
t.stringKeywords(tags)
623+
case "number":
624+
t.numericalKeywords(tags)
625+
case "integer":
626+
t.numericalKeywords(tags)
627+
case "array":
628+
t.arrayKeywords(tags)
629+
case "boolean":
630+
t.booleanKeywords(tags)
631+
}
632+
}
628633
}
634+
629635
extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",")
630636
t.extraKeywords(extras)
631637
}
@@ -643,7 +649,8 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str
643649
case "description":
644650
t.Description = val
645651
case "type":
646-
t.Type = val
652+
types := strings.Split(val, ";")
653+
t.Type = &Type{Types: types}
647654
case "anchor":
648655
t.Anchor = val
649656
case "oneof_required":
@@ -695,11 +702,11 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str
695702
if t.OneOf == nil {
696703
t.OneOf = make([]*Schema, 0, 1)
697704
}
698-
t.Type = ""
705+
t.Type = nil
699706
types := strings.Split(nameValue[1], ";")
700707
for _, ty := range types {
701708
t.OneOf = append(t.OneOf, &Schema{
702-
Type: ty,
709+
Type: &Type{Types: []string{ty}},
703710
})
704711
}
705712
case "anyof_ref":
@@ -721,11 +728,11 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str
721728
if t.AnyOf == nil {
722729
t.AnyOf = make([]*Schema, 0, 1)
723730
}
724-
t.Type = ""
731+
t.Type = nil
725732
types := strings.Split(nameValue[1], ";")
726733
for _, ty := range types {
727734
t.AnyOf = append(t.AnyOf, &Schema{
728-
Type: ty,
735+
Type: &Type{Types: []string{ty}},
729736
})
730737
}
731738
default:
@@ -872,17 +879,23 @@ func (t *Schema) arrayKeywords(tags []string) {
872879
return
873880
}
874881

875-
switch t.Items.Type {
876-
case "string":
877-
t.Items.stringKeywords(unprocessed)
878-
case "number":
879-
t.Items.numericalKeywords(unprocessed)
880-
case "integer":
881-
t.Items.numericalKeywords(unprocessed)
882-
case "array":
883-
// explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong
884-
case "boolean":
885-
t.Items.booleanKeywords(unprocessed)
882+
if t.Items.Type == nil {
883+
return
884+
}
885+
886+
for _, currType := range t.Items.Type.Types {
887+
switch currType {
888+
case "string":
889+
t.Items.stringKeywords(unprocessed)
890+
case "number":
891+
t.Items.numericalKeywords(unprocessed)
892+
case "integer":
893+
t.Items.numericalKeywords(unprocessed)
894+
case "array":
895+
// explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong
896+
case "boolean":
897+
t.Items.booleanKeywords(unprocessed)
898+
}
886899
}
887900
}
888901

@@ -1112,6 +1125,30 @@ func (t *Schema) MarshalJSON() ([]byte, error) {
11121125
return append(b, m[1:]...), nil
11131126
}
11141127

1128+
func (tp Type) MarshalJSON() ([]byte, error) {
1129+
switch len(tp.Types) {
1130+
case 0:
1131+
return nil, nil
1132+
case 1:
1133+
return json.Marshal(tp.Types[0])
1134+
default:
1135+
return json.Marshal(tp.Types)
1136+
}
1137+
}
1138+
1139+
func (tp *Type) UnmarshalJSON(data []byte) error {
1140+
err := json.Unmarshal(data, &tp.Types)
1141+
if err == nil {
1142+
return nil
1143+
}
1144+
var v string
1145+
err2 := json.Unmarshal(data, &v)
1146+
if err2 != nil {
1147+
return fmt.Errorf("could not read type into slice: %v, nor into string: %w", err.Error(), err2)
1148+
}
1149+
return nil
1150+
}
1151+
11151152
func (r *Reflector) typeName(t reflect.Type) string {
11161153
if r.Namer != nil {
11171154
if name := r.Namer(t); name != "" {

reflect_test.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ type CustomTypeFieldWithInterface struct {
125125

126126
func (CustomTimeWithInterface) JSONSchema() *Schema {
127127
return &Schema{
128-
Type: "string",
128+
Type: &Type{Types: []string{"string"}},
129129
Format: "date-time",
130130
}
131131
}
@@ -210,7 +210,7 @@ type UserWithAnchor struct {
210210

211211
func (CompactDate) JSONSchema() *Schema {
212212
return &Schema{
213-
Type: "string",
213+
Type: &Type{Types: []string{"string"}},
214214
Title: "Compact Date",
215215
Description: "Short date that only includes year and month",
216216
Pattern: "^[0-9]{4}-[0-1][0-9]$",
@@ -258,11 +258,11 @@ type CustomSliceType []string
258258
func (CustomSliceType) JSONSchema() *Schema {
259259
return &Schema{
260260
OneOf: []*Schema{{
261-
Type: "string",
261+
Type: &Type{Types: []string{"string"}},
262262
}, {
263-
Type: "array",
263+
Type: &Type{Types: []string{"array"}},
264264
Items: &Schema{
265-
Type: "string",
265+
Type: &Type{Types: []string{"string"}},
266266
},
267267
}},
268268
}
@@ -273,15 +273,15 @@ type CustomMapType map[string]string
273273
func (CustomMapType) JSONSchema() *Schema {
274274
properties := NewProperties()
275275
properties.Set("key", &Schema{
276-
Type: "string",
276+
Type: &Type{Types: []string{"string"}},
277277
})
278278
properties.Set("value", &Schema{
279-
Type: "string",
279+
Type: &Type{Types: []string{"string"}},
280280
})
281281
return &Schema{
282-
Type: "array",
282+
Type: &Type{Types: []string{"array"}},
283283
Items: &Schema{
284-
Type: "object",
284+
Type: &Type{Types: []string{"object"}},
285285
Properties: properties,
286286
Required: []string{"key", "value"},
287287
},
@@ -342,6 +342,10 @@ type PatternEqualsTest struct {
342342
WithEqualsAndCommas string `jsonschema:"pattern=foo\\,=bar"`
343343
}
344344

345+
type MultiTypeTest struct {
346+
Value any `jsonschema:"type=number;object"`
347+
}
348+
345349
func TestReflector(t *testing.T) {
346350
r := new(Reflector)
347351
s := "http://example.com/schema"
@@ -388,7 +392,7 @@ func TestSchemaGeneration(t *testing.T) {
388392
Mapper: func(i reflect.Type) *Schema {
389393
if i == reflect.TypeOf(CustomTime{}) {
390394
return &Schema{
391-
Type: "string",
395+
Type: &Type{Types: []string{"string"}},
392396
Format: "date-time",
393397
}
394398
}
@@ -476,6 +480,7 @@ func TestSchemaGeneration(t *testing.T) {
476480
{SchemaExtendTest{}, &Reflector{}, "fixtures/custom_type_extend.json"},
477481
{Expression{}, &Reflector{}, "fixtures/schema_with_expression.json"},
478482
{PatternEqualsTest{}, &Reflector{}, "fixtures/equals_in_pattern.json"},
483+
{MultiTypeTest{}, &Reflector{}, "fixtures/multi_type_test.json"},
479484
}
480485

481486
for _, tt := range tests {

schema.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type Schema struct {
4040
AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3
4141
PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4
4242
// RFC draft-bhutton-json-schema-validation-00, section 6
43-
Type string `json:"type,omitempty"` // section 6.1.1
43+
Type *Type `json:"type,omitempty"` // section 6.1.1
4444
Enum []any `json:"enum,omitempty"` // section 6.1.2
4545
Const any `json:"const,omitempty"` // section 6.1.3
4646
MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1
@@ -92,3 +92,7 @@ var (
9292
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26
9393
// RFC draft-wright-json-schema-validation-00, section 5.26
9494
type Definitions map[string]*Schema
95+
96+
type Type struct {
97+
Types []string
98+
}

0 commit comments

Comments
 (0)