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
4 changes: 2 additions & 2 deletions jsonschema/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
Package jsonschema is an implementation of the [JSON Schema specification],
a JSON-based format for describing the structure of JSON data.
The package can be used to read schemas for code generation, and to validate
data using the draft 2020-12 specification. Validation with other drafts
or custom meta-schemas is not supported.
data using the draft 2020-12 and draft-07 specifications. Validation with
other drafts or custom meta-schemas is not supported.

Construct a [Schema] as you would any Go struct (for example, by writing
a struct literal), or unmarshal a JSON schema into a [Schema] in the usual
Expand Down
56 changes: 51 additions & 5 deletions jsonschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import (
)

// A Schema is a JSON schema object.
// It corresponds to the 2020-12 draft, as described in https://json-schema.org/draft/2020-12,
// specifically:
// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01
// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01
// It supports both draft-07 and the 2020-12 draft specifications:
// - Draft-07: http://json-schema.org/draft-07/schema#
// - Draft 2020-12: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01
// and https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01
//
// A Schema value may have non-zero values for more than one field:
// all relevant non-zero fields are used for validation.
Expand Down Expand Up @@ -104,6 +104,9 @@ type Schema struct {
PropertyNames *Schema `json:"propertyNames,omitempty"`
UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"`

// draft-07 specific - Dependencies field that was split in draft 2020-12
Dependencies map[string]any `json:"dependencies,omitempty"`

// logic
AllOf []*Schema `json:"allOf,omitempty"`
AnyOf []*Schema `json:"anyOf,omitempty"`
Expand Down Expand Up @@ -176,11 +179,24 @@ func (s *Schema) MarshalJSON() ([]byte, error) {
case s.Types != nil:
typ = s.Types
}

// For draft-07 compatibility: if we only have PrefixItems and no Items,
// marshal PrefixItems as "items" array, otherwise marshal Items as single schema
var items any
if len(s.PrefixItems) > 0 && s.Items == nil {
// This looks like a draft-07 items array converted to prefixItems
items = s.PrefixItems
} else if s.Items != nil {
items = s.Items
}

ms := struct {
Type any `json:"type,omitempty"`
Type any `json:"type,omitempty"`
Items any `json:"items,omitempty"`
*schemaWithoutMethods
}{
Type: typ,
Items: items,
schemaWithoutMethods: (*schemaWithoutMethods)(s),
}
return marshalStructWithMap(&ms, "Extra")
Expand All @@ -202,6 +218,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error {

ms := struct {
Type json.RawMessage `json:"type,omitempty"`
Items json.RawMessage `json:"items,omitempty"`
Const json.RawMessage `json:"const,omitempty"`
MinLength *integer `json:"minLength,omitempty"`
MaxLength *integer `json:"maxLength,omitempty"`
Expand Down Expand Up @@ -267,6 +284,34 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
set(&s.MinContains, ms.MinContains)
set(&s.MaxContains, ms.MaxContains)

// Handle "items" field: can be either a schema or an array of schemas (draft-07)
if len(ms.Items) > 0 {
switch ms.Items[0] {
case '{':
// Single schema object
err = json.Unmarshal(ms.Items, &s.Items)
case '[':
// Array of schemas (draft-07 tuple validation)
// For draft-07, convert items array to prefixItems for compatibility
err = json.Unmarshal(ms.Items, &s.PrefixItems)
case 't', 'f':
// Boolean schema
var boolSchema bool
if err = json.Unmarshal(ms.Items, &boolSchema); err == nil {
if boolSchema {
s.Items = &Schema{}
} else {
s.Items = falseSchema()
}
}
default:
err = fmt.Errorf(`invalid value for "items": %q`, ms.Items)
}
if err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -342,6 +387,7 @@ func (s *Schema) everyChild(f func(*Schema) bool) bool {
}
}
}

return true
}

Expand Down
80 changes: 74 additions & 6 deletions jsonschema/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,34 @@ import (
"github.com/modelcontextprotocol/go-sdk/internal/util"
)

// The value of the "$schema" keyword for the version that we can validate.
const draft202012 = "https://json-schema.org/draft/2020-12/schema"
// The values of the "$schema" keyword for the versions that we can validate.
const (
draft07 = "http://json-schema.org/draft-07/schema#"
draft07Sec = "https://json-schema.org/draft-07/schema#"
draft202012 = "https://json-schema.org/draft/2020-12/schema"
)

// isValidSchemaVersion checks if the given schema version is supported
func isValidSchemaVersion(version string) bool {
return version == "" || version == draft07 || version == draft07Sec || version == draft202012
}

// isDraft07 checks if the schema version is draft-07
func isDraft07(version string) bool {
return version == draft07 || version == draft07Sec
}

// isDraft202012 checks if the schema version is draft 2020-12
func isDraft202012(version string) bool {
return version == "" || version == draft202012 // empty defaults to 2020-12
}

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

// For draft 2020-12: items applies to remaining items after prefixItems
// For draft-07: additionalItems applies to remaining items after items array
if schema.Items != nil {
for i := len(schema.PrefixItems); i < instance.Len(); i++ {
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
Expand All @@ -320,6 +343,14 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
}
// Note that all the items in this array have been validated.
anns.allItems = true
} else if schema.AdditionalItems != nil {
// Draft-07 style: use additionalItems for remaining items
for i := len(schema.PrefixItems); i < instance.Len(); i++ {
if err := st.validate(instance.Index(i), schema.AdditionalItems, nil); err != nil {
return err
}
}
anns.allItems = true
}

nContains := 0
Expand Down Expand Up @@ -524,6 +555,43 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
}
}
}

// Draft-07 dependencies (combines both property and schema dependencies)
if schema.Dependencies != nil {
for dprop, dep := range schema.Dependencies {
if hasProperty(dprop) {
switch v := dep.(type) {
case []interface{}:
// Array of strings - property dependencies
var reqs []string
for _, item := range v {
if str, ok := item.(string); ok {
reqs = append(reqs, str)
}
}
if m := missingProperties(reqs); len(m) > 0 {
return fmt.Errorf("dependencies[%q]: missing properties %q", dprop, m)
}
case map[string]interface{}:
// Schema object - schema dependencies
// Convert map to Schema and resolve it properly
if data, err := json.Marshal(v); err == nil {
var depSchema Schema
if err := json.Unmarshal(data, &depSchema); err == nil {
// Resolve the dependency schema
resolved, err := depSchema.Resolve(nil)
if err != nil {
return fmt.Errorf("dependencies[%q]: failed to resolve schema: %w", dprop, err)
}
if err := resolved.Validate(instance.Interface()); err != nil {
return fmt.Errorf("dependencies[%q]: %w", dprop, err)
}
}
}
}
}
}
}
if schema.UnevaluatedProperties != nil && !anns.allProperties {
// This looks a lot like AdditionalProperties, but depends on in-place keywords like allOf
// in addition to sibling keywords.
Expand Down
Loading