Skip to content

Commit 936df35

Browse files
1 parent b34ba21 commit 936df35

File tree

3 files changed

+128
-13
lines changed

3 files changed

+128
-13
lines changed

jsonschema/doc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
Package jsonschema is an implementation of the [JSON Schema specification],
77
a JSON-based format for describing the structure of JSON data.
88
The package can be used to read schemas for code generation, and to validate
9-
data using the draft 2020-12 specification. Validation with other drafts
10-
or custom meta-schemas is not supported.
9+
data using the draft 2020-12 and draft-07 specifications. Validation with
10+
other drafts or custom meta-schemas is not supported.
1111
1212
Construct a [Schema] as you would any Go struct (for example, by writing
1313
a struct literal), or unmarshal a JSON schema into a [Schema] in the usual

jsonschema/schema.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ import (
2020
)
2121

2222
// A Schema is a JSON schema object.
23-
// It corresponds to the 2020-12 draft, as described in https://json-schema.org/draft/2020-12,
24-
// specifically:
25-
// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01
26-
// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01
23+
// It supports both draft-07 and the 2020-12 draft specifications:
24+
// - Draft-07: http://json-schema.org/draft-07/schema#
25+
// - Draft 2020-12: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01
26+
// and https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01
2727
//
2828
// A Schema value may have non-zero values for more than one field:
2929
// all relevant non-zero fields are used for validation.
@@ -103,6 +103,9 @@ type Schema struct {
103103
AdditionalProperties *Schema `json:"additionalProperties,omitempty"`
104104
PropertyNames *Schema `json:"propertyNames,omitempty"`
105105
UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"`
106+
107+
// draft-07 specific - Dependencies field that was split in draft 2020-12
108+
Dependencies map[string]any `json:"dependencies,omitempty"`
106109

107110
// logic
108111
AllOf []*Schema `json:"allOf,omitempty"`
@@ -176,11 +179,24 @@ func (s *Schema) MarshalJSON() ([]byte, error) {
176179
case s.Types != nil:
177180
typ = s.Types
178181
}
182+
183+
// For draft-07 compatibility: if we only have PrefixItems and no Items,
184+
// marshal PrefixItems as "items" array, otherwise marshal Items as single schema
185+
var items any
186+
if len(s.PrefixItems) > 0 && s.Items == nil {
187+
// This looks like a draft-07 items array converted to prefixItems
188+
items = s.PrefixItems
189+
} else if s.Items != nil {
190+
items = s.Items
191+
}
192+
179193
ms := struct {
180-
Type any `json:"type,omitempty"`
194+
Type any `json:"type,omitempty"`
195+
Items any `json:"items,omitempty"`
181196
*schemaWithoutMethods
182197
}{
183198
Type: typ,
199+
Items: items,
184200
schemaWithoutMethods: (*schemaWithoutMethods)(s),
185201
}
186202
return marshalStructWithMap(&ms, "Extra")
@@ -202,6 +218,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
202218

203219
ms := struct {
204220
Type json.RawMessage `json:"type,omitempty"`
221+
Items json.RawMessage `json:"items,omitempty"`
205222
Const json.RawMessage `json:"const,omitempty"`
206223
MinLength *integer `json:"minLength,omitempty"`
207224
MaxLength *integer `json:"maxLength,omitempty"`
@@ -267,6 +284,34 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
267284
set(&s.MinContains, ms.MinContains)
268285
set(&s.MaxContains, ms.MaxContains)
269286

287+
// Handle "items" field: can be either a schema or an array of schemas (draft-07)
288+
if len(ms.Items) > 0 {
289+
switch ms.Items[0] {
290+
case '{':
291+
// Single schema object
292+
err = json.Unmarshal(ms.Items, &s.Items)
293+
case '[':
294+
// Array of schemas (draft-07 tuple validation)
295+
// For draft-07, convert items array to prefixItems for compatibility
296+
err = json.Unmarshal(ms.Items, &s.PrefixItems)
297+
case 't', 'f':
298+
// Boolean schema
299+
var boolSchema bool
300+
if err = json.Unmarshal(ms.Items, &boolSchema); err == nil {
301+
if boolSchema {
302+
s.Items = &Schema{}
303+
} else {
304+
s.Items = falseSchema()
305+
}
306+
}
307+
default:
308+
err = fmt.Errorf(`invalid value for "items": %q`, ms.Items)
309+
}
310+
if err != nil {
311+
return err
312+
}
313+
}
314+
270315
return nil
271316
}
272317

@@ -342,6 +387,8 @@ func (s *Schema) everyChild(f func(*Schema) bool) bool {
342387
}
343388
}
344389
}
390+
391+
345392
return true
346393
}
347394

jsonschema/validate.go

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,34 @@ import (
2020
"github.com/modelcontextprotocol/go-sdk/internal/util"
2121
)
2222

23-
// The value of the "$schema" keyword for the version that we can validate.
24-
const draft202012 = "https://json-schema.org/draft/2020-12/schema"
23+
// The values of the "$schema" keyword for the versions that we can validate.
24+
const (
25+
draft07 = "http://json-schema.org/draft-07/schema#"
26+
draft07Sec = "https://json-schema.org/draft-07/schema#"
27+
draft202012 = "https://json-schema.org/draft/2020-12/schema"
28+
)
29+
30+
// isValidSchemaVersion checks if the given schema version is supported
31+
func isValidSchemaVersion(version string) bool {
32+
return version == "" || version == draft07 || version == draft07Sec || version == draft202012
33+
}
34+
35+
// isDraft07 checks if the schema version is draft-07
36+
func isDraft07(version string) bool {
37+
return version == draft07 || version == draft07Sec
38+
}
39+
40+
// isDraft202012 checks if the schema version is draft 2020-12
41+
func isDraft202012(version string) bool {
42+
return version == "" || version == draft202012 // empty defaults to 2020-12
43+
}
2544

2645
// Validate validates the instance, which must be a JSON value, against the schema.
2746
// It returns nil if validation is successful or an error if it is not.
2847
// If the schema type is "object", instance can be a map[string]any or a struct.
2948
func (rs *Resolved) Validate(instance any) error {
30-
if s := rs.root.Schema; s != "" && s != draft202012 {
31-
return fmt.Errorf("cannot validate version %s, only %s", s, draft202012)
49+
if s := rs.root.Schema; !isValidSchemaVersion(s) {
50+
return fmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", s)
3251
}
3352
st := &state{rs: rs}
3453
return st.validate(reflect.ValueOf(instance), st.rs.root, nil)
@@ -40,8 +59,8 @@ func (rs *Resolved) Validate(instance any) error {
4059
// TODO(jba): account for dynamic refs. This algorithm simple-mindedly
4160
// treats each schema with a default as its own root.
4261
func (rs *Resolved) validateDefaults() error {
43-
if s := rs.root.Schema; s != "" && s != draft202012 {
44-
return fmt.Errorf("cannot validate version %s, only %s", s, draft202012)
62+
if s := rs.root.Schema; !isValidSchemaVersion(s) {
63+
return fmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", s)
4564
}
4665
st := &state{rs: rs}
4766
for s := range rs.root.all() {
@@ -298,6 +317,8 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
298317
// arrays
299318
// TODO(jba): consider arrays of structs.
300319
if instance.Kind() == reflect.Array || instance.Kind() == reflect.Slice {
320+
// Handle both draft-07 and draft 2020-12 array validation using the same logic
321+
// Draft-07 items arrays are converted to prefixItems during unmarshaling
301322
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.1
302323
// This validate call doesn't collect annotations for the items of the instance; they are separate
303324
// instances in their own right.
@@ -312,6 +333,8 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
312333
}
313334
anns.noteEndIndex(min(len(schema.PrefixItems), instance.Len()))
314335

336+
// For draft 2020-12: items applies to remaining items after prefixItems
337+
// For draft-07: additionalItems applies to remaining items after items array
315338
if schema.Items != nil {
316339
for i := len(schema.PrefixItems); i < instance.Len(); i++ {
317340
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
@@ -320,6 +343,14 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
320343
}
321344
// Note that all the items in this array have been validated.
322345
anns.allItems = true
346+
} else if schema.AdditionalItems != nil {
347+
// Draft-07 style: use additionalItems for remaining items
348+
for i := len(schema.PrefixItems); i < instance.Len(); i++ {
349+
if err := st.validate(instance.Index(i), schema.AdditionalItems, nil); err != nil {
350+
return err
351+
}
352+
}
353+
anns.allItems = true
323354
}
324355

325356
nContains := 0
@@ -524,6 +555,43 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
524555
}
525556
}
526557
}
558+
559+
// Draft-07 dependencies (combines both property and schema dependencies)
560+
if schema.Dependencies != nil {
561+
for dprop, dep := range schema.Dependencies {
562+
if hasProperty(dprop) {
563+
switch v := dep.(type) {
564+
case []interface{}:
565+
// Array of strings - property dependencies
566+
var reqs []string
567+
for _, item := range v {
568+
if str, ok := item.(string); ok {
569+
reqs = append(reqs, str)
570+
}
571+
}
572+
if m := missingProperties(reqs); len(m) > 0 {
573+
return fmt.Errorf("dependencies[%q]: missing properties %q", dprop, m)
574+
}
575+
case map[string]interface{}:
576+
// Schema object - schema dependencies
577+
// Convert map to Schema and resolve it properly
578+
if data, err := json.Marshal(v); err == nil {
579+
var depSchema Schema
580+
if err := json.Unmarshal(data, &depSchema); err == nil {
581+
// Resolve the dependency schema
582+
resolved, err := depSchema.Resolve(nil)
583+
if err != nil {
584+
return fmt.Errorf("dependencies[%q]: failed to resolve schema: %w", dprop, err)
585+
}
586+
if err := resolved.Validate(instance.Interface()); err != nil {
587+
return fmt.Errorf("dependencies[%q]: %w", dprop, err)
588+
}
589+
}
590+
}
591+
}
592+
}
593+
}
594+
}
527595
if schema.UnevaluatedProperties != nil && !anns.allProperties {
528596
// This looks a lot like AdditionalProperties, but depends on in-place keywords like allOf
529597
// in addition to sibling keywords.

0 commit comments

Comments
 (0)