Skip to content

Commit c650976

Browse files
authored
Support $ref and $defs in JSON Schema (#1030)
* support $ref and $defs in JSON Schema * update
1 parent bd612ce commit c650976

File tree

4 files changed

+340
-18
lines changed

4 files changed

+340
-18
lines changed

jsonschema/json.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ type Definition struct {
4848
AdditionalProperties any `json:"additionalProperties,omitempty"`
4949
// Whether the schema is nullable or not.
5050
Nullable bool `json:"nullable,omitempty"`
51+
52+
// Ref Reference to a definition in $defs or external schema.
53+
Ref string `json:"$ref,omitempty"`
54+
// Defs A map of reusable schema definitions.
55+
Defs map[string]Definition `json:"$defs,omitempty"`
5156
}
5257

5358
func (d *Definition) MarshalJSON() ([]byte, error) {
@@ -67,10 +72,16 @@ func (d *Definition) Unmarshal(content string, v any) error {
6772
}
6873

6974
func GenerateSchemaForType(v any) (*Definition, error) {
70-
return reflectSchema(reflect.TypeOf(v))
75+
var defs = make(map[string]Definition)
76+
def, err := reflectSchema(reflect.TypeOf(v), defs)
77+
if err != nil {
78+
return nil, err
79+
}
80+
def.Defs = defs
81+
return def, nil
7182
}
7283

73-
func reflectSchema(t reflect.Type) (*Definition, error) {
84+
func reflectSchema(t reflect.Type, defs map[string]Definition) (*Definition, error) {
7485
var d Definition
7586
switch t.Kind() {
7687
case reflect.String:
@@ -84,21 +95,32 @@ func reflectSchema(t reflect.Type) (*Definition, error) {
8495
d.Type = Boolean
8596
case reflect.Slice, reflect.Array:
8697
d.Type = Array
87-
items, err := reflectSchema(t.Elem())
98+
items, err := reflectSchema(t.Elem(), defs)
8899
if err != nil {
89100
return nil, err
90101
}
91102
d.Items = items
92103
case reflect.Struct:
104+
if t.Name() != "" {
105+
if _, ok := defs[t.Name()]; !ok {
106+
defs[t.Name()] = Definition{}
107+
object, err := reflectSchemaObject(t, defs)
108+
if err != nil {
109+
return nil, err
110+
}
111+
defs[t.Name()] = *object
112+
}
113+
return &Definition{Ref: "#/$defs/" + t.Name()}, nil
114+
}
93115
d.Type = Object
94116
d.AdditionalProperties = false
95-
object, err := reflectSchemaObject(t)
117+
object, err := reflectSchemaObject(t, defs)
96118
if err != nil {
97119
return nil, err
98120
}
99121
d = *object
100122
case reflect.Ptr:
101-
definition, err := reflectSchema(t.Elem())
123+
definition, err := reflectSchema(t.Elem(), defs)
102124
if err != nil {
103125
return nil, err
104126
}
@@ -112,7 +134,7 @@ func reflectSchema(t reflect.Type) (*Definition, error) {
112134
return &d, nil
113135
}
114136

115-
func reflectSchemaObject(t reflect.Type) (*Definition, error) {
137+
func reflectSchemaObject(t reflect.Type, defs map[string]Definition) (*Definition, error) {
116138
var d = Definition{
117139
Type: Object,
118140
AdditionalProperties: false,
@@ -136,7 +158,7 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) {
136158
required = false
137159
}
138160

139-
item, err := reflectSchema(field.Type)
161+
item, err := reflectSchema(field.Type, defs)
140162
if err != nil {
141163
return nil, err
142164
}

jsonschema/json_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,17 @@ func TestDefinition_MarshalJSON(t *testing.T) {
183183
}
184184

185185
func TestStructToSchema(t *testing.T) {
186+
type Tweet struct {
187+
Text string `json:"text"`
188+
}
189+
190+
type Person struct {
191+
Name string `json:"name,omitempty"`
192+
Age int `json:"age,omitempty"`
193+
Friends []Person `json:"friends,omitempty"`
194+
Tweets []Tweet `json:"tweets,omitempty"`
195+
}
196+
186197
tests := []struct {
187198
name string
188199
in any
@@ -376,6 +387,65 @@ func TestStructToSchema(t *testing.T) {
376387
"additionalProperties":false
377388
}`,
378389
},
390+
{
391+
name: "Test with $ref and $defs",
392+
in: struct {
393+
Person Person `json:"person"`
394+
Tweets []Tweet `json:"tweets"`
395+
}{},
396+
want: `{
397+
"type" : "object",
398+
"properties" : {
399+
"person" : {
400+
"$ref" : "#/$defs/Person"
401+
},
402+
"tweets" : {
403+
"type" : "array",
404+
"items" : {
405+
"$ref" : "#/$defs/Tweet"
406+
}
407+
}
408+
},
409+
"required" : [ "person", "tweets" ],
410+
"additionalProperties" : false,
411+
"$defs" : {
412+
"Person" : {
413+
"type" : "object",
414+
"properties" : {
415+
"age" : {
416+
"type" : "integer"
417+
},
418+
"friends" : {
419+
"type" : "array",
420+
"items" : {
421+
"$ref" : "#/$defs/Person"
422+
}
423+
},
424+
"name" : {
425+
"type" : "string"
426+
},
427+
"tweets" : {
428+
"type" : "array",
429+
"items" : {
430+
"$ref" : "#/$defs/Tweet"
431+
}
432+
}
433+
},
434+
"additionalProperties" : false
435+
},
436+
"Tweet" : {
437+
"type" : "object",
438+
"properties" : {
439+
"text" : {
440+
"type" : "string"
441+
}
442+
},
443+
"required" : [ "text" ],
444+
"additionalProperties" : false
445+
}
446+
}
447+
}`,
448+
},
379449
}
380450

381451
for _, tt := range tests {

jsonschema/validate.go

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,68 @@ import (
55
"errors"
66
)
77

8+
func CollectDefs(def Definition) map[string]Definition {
9+
result := make(map[string]Definition)
10+
collectDefsRecursive(def, result, "#")
11+
return result
12+
}
13+
14+
func collectDefsRecursive(def Definition, result map[string]Definition, prefix string) {
15+
for k, v := range def.Defs {
16+
path := prefix + "/$defs/" + k
17+
result[path] = v
18+
collectDefsRecursive(v, result, path)
19+
}
20+
for k, sub := range def.Properties {
21+
collectDefsRecursive(sub, result, prefix+"/properties/"+k)
22+
}
23+
if def.Items != nil {
24+
collectDefsRecursive(*def.Items, result, prefix)
25+
}
26+
}
27+
828
func VerifySchemaAndUnmarshal(schema Definition, content []byte, v any) error {
929
var data any
1030
err := json.Unmarshal(content, &data)
1131
if err != nil {
1232
return err
1333
}
14-
if !Validate(schema, data) {
34+
if !Validate(schema, data, WithDefs(CollectDefs(schema))) {
1535
return errors.New("data validation failed against the provided schema")
1636
}
1737
return json.Unmarshal(content, &v)
1838
}
1939

20-
func Validate(schema Definition, data any) bool {
40+
type validateArgs struct {
41+
Defs map[string]Definition
42+
}
43+
44+
type ValidateOption func(*validateArgs)
45+
46+
func WithDefs(defs map[string]Definition) ValidateOption {
47+
return func(option *validateArgs) {
48+
option.Defs = defs
49+
}
50+
}
51+
52+
func Validate(schema Definition, data any, opts ...ValidateOption) bool {
53+
args := validateArgs{}
54+
for _, opt := range opts {
55+
opt(&args)
56+
}
57+
if len(opts) == 0 {
58+
args.Defs = CollectDefs(schema)
59+
}
2160
switch schema.Type {
2261
case Object:
23-
return validateObject(schema, data)
62+
return validateObject(schema, data, args.Defs)
2463
case Array:
25-
return validateArray(schema, data)
64+
return validateArray(schema, data, args.Defs)
2665
case String:
27-
_, ok := data.(string)
66+
v, ok := data.(string)
67+
if ok && len(schema.Enum) > 0 {
68+
return contains(schema.Enum, v)
69+
}
2870
return ok
2971
case Number: // float64 and int
3072
_, ok := data.(float64)
@@ -45,11 +87,16 @@ func Validate(schema Definition, data any) bool {
4587
case Null:
4688
return data == nil
4789
default:
90+
if schema.Ref != "" && args.Defs != nil {
91+
if v, ok := args.Defs[schema.Ref]; ok {
92+
return Validate(v, data, WithDefs(args.Defs))
93+
}
94+
}
4895
return false
4996
}
5097
}
5198

52-
func validateObject(schema Definition, data any) bool {
99+
func validateObject(schema Definition, data any, defs map[string]Definition) bool {
53100
dataMap, ok := data.(map[string]any)
54101
if !ok {
55102
return false
@@ -61,7 +108,7 @@ func validateObject(schema Definition, data any) bool {
61108
}
62109
for key, valueSchema := range schema.Properties {
63110
value, exists := dataMap[key]
64-
if exists && !Validate(valueSchema, value) {
111+
if exists && !Validate(valueSchema, value, WithDefs(defs)) {
65112
return false
66113
} else if !exists && contains(schema.Required, key) {
67114
return false
@@ -70,13 +117,13 @@ func validateObject(schema Definition, data any) bool {
70117
return true
71118
}
72119

73-
func validateArray(schema Definition, data any) bool {
120+
func validateArray(schema Definition, data any, defs map[string]Definition) bool {
74121
dataArray, ok := data.([]any)
75122
if !ok {
76123
return false
77124
}
78125
for _, item := range dataArray {
79-
if !Validate(*schema.Items, item) {
126+
if !Validate(*schema.Items, item, WithDefs(defs)) {
80127
return false
81128
}
82129
}

0 commit comments

Comments
 (0)