fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+76
@@ -0,0 +1,76 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import "maps"
|
||||
|
||||
// An annotations tracks certain properties computed by keywords that are used by validation.
|
||||
// ("Annotation" is the spec's term.)
|
||||
// In particular, the unevaluatedItems and unevaluatedProperties keywords need to know which
|
||||
// items and properties were evaluated (validated successfully).
|
||||
type annotations struct {
|
||||
allItems bool // all items were evaluated
|
||||
endIndex int // 1+largest index evaluated by prefixItems
|
||||
evaluatedIndexes map[int]bool // set of indexes evaluated by contains
|
||||
allProperties bool // all properties were evaluated
|
||||
evaluatedProperties map[string]bool // set of properties evaluated by various keywords
|
||||
}
|
||||
|
||||
// noteIndex marks i as evaluated.
|
||||
func (a *annotations) noteIndex(i int) {
|
||||
if a.evaluatedIndexes == nil {
|
||||
a.evaluatedIndexes = map[int]bool{}
|
||||
}
|
||||
a.evaluatedIndexes[i] = true
|
||||
}
|
||||
|
||||
// noteEndIndex marks items with index less than end as evaluated.
|
||||
func (a *annotations) noteEndIndex(end int) {
|
||||
if end > a.endIndex {
|
||||
a.endIndex = end
|
||||
}
|
||||
}
|
||||
|
||||
// noteProperty marks prop as evaluated.
|
||||
func (a *annotations) noteProperty(prop string) {
|
||||
if a.evaluatedProperties == nil {
|
||||
a.evaluatedProperties = map[string]bool{}
|
||||
}
|
||||
a.evaluatedProperties[prop] = true
|
||||
}
|
||||
|
||||
// noteProperties marks all the properties in props as evaluated.
|
||||
func (a *annotations) noteProperties(props map[string]bool) {
|
||||
a.evaluatedProperties = merge(a.evaluatedProperties, props)
|
||||
}
|
||||
|
||||
// merge adds b's annotations to a.
|
||||
// a must not be nil.
|
||||
func (a *annotations) merge(b *annotations) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
if b.allItems {
|
||||
a.allItems = true
|
||||
}
|
||||
if b.endIndex > a.endIndex {
|
||||
a.endIndex = b.endIndex
|
||||
}
|
||||
a.evaluatedIndexes = merge(a.evaluatedIndexes, b.evaluatedIndexes)
|
||||
if b.allProperties {
|
||||
a.allProperties = true
|
||||
}
|
||||
a.evaluatedProperties = merge(a.evaluatedProperties, b.evaluatedProperties)
|
||||
}
|
||||
|
||||
// merge adds t's keys to s and returns s.
|
||||
// If s is nil, it returns a copy of t.
|
||||
func merge[K comparable](s, t map[K]bool) map[K]bool {
|
||||
if s == nil {
|
||||
return maps.Clone(t)
|
||||
}
|
||||
maps.Copy(s, t)
|
||||
return s
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
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 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
|
||||
way (with [encoding/json], for instance). It can then be used for code
|
||||
generation or other purposes without further processing.
|
||||
You can also infer a schema from a Go struct.
|
||||
|
||||
# Resolution
|
||||
|
||||
A Schema can refer to other schemas, both inside and outside itself. These
|
||||
references must be resolved before a schema can be used for validation.
|
||||
Call [Schema.Resolve] to obtain a resolved schema (called a [Resolved]).
|
||||
If the schema has external references, pass a [ResolveOptions] with a [Loader]
|
||||
to load them. To validate default values in a schema, set
|
||||
[ResolveOptions.ValidateDefaults] to true.
|
||||
|
||||
# Validation
|
||||
|
||||
Call [Resolved.Validate] to validate a JSON value. The value must be a
|
||||
Go value that looks like the result of unmarshaling a JSON value into an
|
||||
[any] or a struct. For example, the JSON value
|
||||
|
||||
{"name": "Al", "scores": [90, 80, 100]}
|
||||
|
||||
could be represented as the Go value
|
||||
|
||||
map[string]any{
|
||||
"name": "Al",
|
||||
"scores": []any{90, 80, 100},
|
||||
}
|
||||
|
||||
or as a value of this type:
|
||||
|
||||
type Player struct {
|
||||
Name string `json:"name"`
|
||||
Scores []int `json:"scores"`
|
||||
}
|
||||
|
||||
# Inference
|
||||
|
||||
The [For] function returns a [Schema] describing the given Go type.
|
||||
Each field in the struct becomes a property of the schema.
|
||||
The values of "json" tags are respected: the field's property name is taken
|
||||
from the tag, and fields omitted from the JSON are omitted from the schema as
|
||||
well.
|
||||
For example, `jsonschema.For[Player]()` returns this schema:
|
||||
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"}
|
||||
}
|
||||
"required": ["name", "scores"],
|
||||
"additionalProperties": {"not": {}}
|
||||
}
|
||||
}
|
||||
|
||||
Use the "jsonschema" struct tag to provide a description for the property:
|
||||
|
||||
type Player struct {
|
||||
Name string `json:"name" jsonschema:"player name"`
|
||||
Scores []int `json:"scores" jsonschema:"scores of player's games"`
|
||||
}
|
||||
|
||||
# Deviations from the specification
|
||||
|
||||
Regular expressions are processed with Go's regexp package, which differs
|
||||
from ECMA 262, most significantly in not supporting back-references.
|
||||
See [this table of differences] for more.
|
||||
|
||||
The "format" keyword described in [section 7 of the validation spec] is recorded
|
||||
in the Schema, but is ignored during validation.
|
||||
It does not even produce [annotations].
|
||||
Use the "pattern" keyword instead: it will work more reliably across JSON Schema
|
||||
implementations. See [learnjsonschema.com] for more recommendations about "format".
|
||||
|
||||
The content keywords described in [section 8 of the validation spec]
|
||||
are recorded in the schema, but ignored during validation.
|
||||
|
||||
# Controlling behavior changes
|
||||
|
||||
Minor and patch releases of this package may introduce behavior changes as part
|
||||
of bug fixes or correctness improvements. To help manage the impact of such
|
||||
changes, the package allows you to access previous behaviors using the
|
||||
`JSONSCHEMAGODEBUG` environment variable. The available settings are listed
|
||||
below; additional options may be introduced in future releases.
|
||||
|
||||
- **typeschemasnull**: When set to `"1"`, the inferred schema for slices will
|
||||
*not* include the `null` type alongside the array type. It will also avoid
|
||||
adding `null` to non-native pointer types (such as `time.Time`). This restores
|
||||
the behavior from versions prior to v0.3.0. The default behavior is to include
|
||||
`null` in these cases.
|
||||
|
||||
[JSON Schema specification]: https://json-schema.org
|
||||
[section 7 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
|
||||
[section 8 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
|
||||
[learnjsonschema.com]: https://www.learnjsonschema.com/2020-12/format-annotation/format/
|
||||
[this table of differences]: https://github.com/dlclark/regexp2?tab=readme-ov-file#compare-regexp-and-regexp2
|
||||
[annotations]: https://json-schema.org/draft/2020-12/json-schema-core#name-annotations
|
||||
*/
|
||||
package jsonschema
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file contains functions that infer a schema from a Go type.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
const debugEnv = "JSONSCHEMAGODEBUG"
|
||||
|
||||
// ForOptions are options for the [For] and [ForType] functions.
|
||||
type ForOptions struct {
|
||||
// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
|
||||
// Schema are ignored instead of causing an error.
|
||||
// This allows callers to adjust the resulting schema using custom knowledge.
|
||||
// For example, an interface type where all the possible implementations are
|
||||
// known can be described with "oneof".
|
||||
IgnoreInvalidTypes bool
|
||||
|
||||
// TypeSchemas maps types to their schemas.
|
||||
// If [For] encounters a type that is a key in this map, the
|
||||
// corresponding value is used as the resulting schema (after cloning to
|
||||
// ensure uniqueness).
|
||||
// Types in this map override the default translations, as described
|
||||
// in [For]'s documentation.
|
||||
// PropertyOrder defined in these schemas will not be used in [For] or [ForType].
|
||||
TypeSchemas map[reflect.Type]*Schema
|
||||
}
|
||||
|
||||
// For constructs a JSON schema object for the given type argument.
|
||||
// If non-nil, the provided options configure certain aspects of this contruction,
|
||||
// described below.
|
||||
|
||||
// It translates Go types into compatible JSON schema types, as follows.
|
||||
// These defaults can be overridden by [ForOptions.TypeSchemas].
|
||||
//
|
||||
// - Strings have schema type "string".
|
||||
// - Bools have schema type "boolean".
|
||||
// - Signed and unsigned integer types have schema type "integer".
|
||||
// - Floating point types have schema type "number".
|
||||
// - Slices and arrays have schema type "array", and a corresponding schema
|
||||
// for items.
|
||||
// - Maps with string key have schema type "object", and corresponding
|
||||
// schema for additionalProperties.
|
||||
// - Structs have schema type "object", and disallow additionalProperties.
|
||||
// Their properties are derived from exported struct fields, using the
|
||||
// struct field JSON name. Fields that are marked "omitempty" or "omitzero" are
|
||||
// considered optional; all other fields become required properties.
|
||||
// For structs, the PropertyOrder will be set to the field order.
|
||||
// - Some types in the standard library that implement json.Marshaler
|
||||
// translate to schemas that match the values to which they marshal.
|
||||
// For example, [time.Time] translates to the schema for strings.
|
||||
//
|
||||
// For will return an error if there is a cycle in the types.
|
||||
//
|
||||
// By default, For returns an error if t contains (possibly recursively) any of the
|
||||
// following Go types, as they are incompatible with the JSON schema spec.
|
||||
// If [ForOptions.IgnoreInvalidTypes] is true, then these types are ignored instead.
|
||||
// - maps with key other than 'string'
|
||||
// - function types
|
||||
// - channel types
|
||||
// - complex numbers
|
||||
// - unsafe pointers
|
||||
//
|
||||
// This function recognizes struct field tags named "jsonschema".
|
||||
// A jsonschema tag on a field is used as the description for the corresponding property.
|
||||
// For future compatibility, descriptions must not start with "WORD=", where WORD is a
|
||||
// sequence of non-whitespace characters.
|
||||
func For[T any](opts *ForOptions) (*Schema, error) {
|
||||
if opts == nil {
|
||||
opts = &ForOptions{}
|
||||
}
|
||||
schemas := maps.Clone(initialSchemaMap)
|
||||
// Add types from the options. They override the default ones.
|
||||
maps.Copy(schemas, opts.TypeSchemas)
|
||||
s, err := forType(reflect.TypeFor[T](), map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
|
||||
if err != nil {
|
||||
var z T
|
||||
return nil, fmt.Errorf("For[%T](): %w", z, err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ForType is like [For], but takes a [reflect.Type]
|
||||
func ForType(t reflect.Type, opts *ForOptions) (*Schema, error) {
|
||||
if opts == nil {
|
||||
opts = &ForOptions{}
|
||||
}
|
||||
schemas := maps.Clone(initialSchemaMap)
|
||||
// Add types from the options. They override the default ones.
|
||||
maps.Copy(schemas, opts.TypeSchemas)
|
||||
s, err := forType(t, map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ForType(%s): %w", t, err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Helper to create a *float64 pointer from a value
|
||||
func f64Ptr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas map[reflect.Type]*Schema) (*Schema, error) {
|
||||
// Follow pointers: the schema for *T is almost the same as for T, except that
|
||||
// an explicit JSON "null" is allowed for the pointer.
|
||||
allowNull := false
|
||||
for t.Kind() == reflect.Pointer {
|
||||
allowNull = true
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// Check for cycles
|
||||
// User defined types have a name, so we can skip those that are natively defined
|
||||
if t.Name() != "" {
|
||||
if seen[t] {
|
||||
return nil, fmt.Errorf("cycle detected for type %v", t)
|
||||
}
|
||||
seen[t] = true
|
||||
defer delete(seen, t)
|
||||
}
|
||||
|
||||
if s := schemas[t]; s != nil {
|
||||
cloned := s.CloneSchemas()
|
||||
if os.Getenv(debugEnv) != "typeschemasnull=1" && allowNull {
|
||||
if cloned.Type != "" {
|
||||
cloned.Types = []string{"null", cloned.Type}
|
||||
cloned.Type = ""
|
||||
} else if !slices.Contains(cloned.Types, "null") {
|
||||
cloned.Types = append([]string{"null"}, cloned.Types...)
|
||||
}
|
||||
}
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
var (
|
||||
s = new(Schema)
|
||||
err error
|
||||
)
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
s.Type = "boolean"
|
||||
|
||||
case reflect.Int, reflect.Int64:
|
||||
s.Type = "integer"
|
||||
|
||||
case reflect.Uint, reflect.Uint64, reflect.Uintptr:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
|
||||
case reflect.Int8:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(math.MinInt8)
|
||||
s.Maximum = f64Ptr(math.MaxInt8)
|
||||
|
||||
case reflect.Uint8:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
s.Maximum = f64Ptr(math.MaxUint8)
|
||||
|
||||
case reflect.Int16:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(math.MinInt16)
|
||||
s.Maximum = f64Ptr(math.MaxInt16)
|
||||
|
||||
case reflect.Uint16:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
s.Maximum = f64Ptr(math.MaxUint16)
|
||||
|
||||
case reflect.Int32:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(math.MinInt32)
|
||||
s.Maximum = f64Ptr(math.MaxInt32)
|
||||
|
||||
case reflect.Uint32:
|
||||
s.Type = "integer"
|
||||
s.Minimum = f64Ptr(0)
|
||||
s.Maximum = f64Ptr(math.MaxUint32)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s.Type = "number"
|
||||
|
||||
case reflect.Interface:
|
||||
// Unrestricted
|
||||
|
||||
case reflect.Map:
|
||||
if t.Key().Kind() != reflect.String && !t.Key().Implements(reflect.TypeFor[encoding.TextMarshaler]()) {
|
||||
if ignore {
|
||||
return nil, nil // ignore
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind())
|
||||
}
|
||||
s.Type = "object"
|
||||
s.AdditionalProperties, err = forType(t.Elem(), seen, ignore, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing map value schema: %v", err)
|
||||
}
|
||||
if ignore && s.AdditionalProperties == nil {
|
||||
// Ignore if the element type is invalid.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
if os.Getenv(debugEnv) != "typeschemasnull=1" && t.Kind() == reflect.Slice {
|
||||
s.Types = []string{"null", "array"}
|
||||
} else {
|
||||
s.Type = "array"
|
||||
}
|
||||
itemsSchema, err := forType(t.Elem(), seen, ignore, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing element schema: %v", err)
|
||||
}
|
||||
if itemsSchema == nil {
|
||||
return nil, nil
|
||||
}
|
||||
s.Items = itemsSchema
|
||||
if ignore && s.Items == nil {
|
||||
// Ignore if the element type is invalid.
|
||||
return nil, nil
|
||||
}
|
||||
if t.Kind() == reflect.Array {
|
||||
s.MinItems = Ptr(t.Len())
|
||||
s.MaxItems = Ptr(t.Len())
|
||||
}
|
||||
|
||||
case reflect.String:
|
||||
s.Type = "string"
|
||||
|
||||
case reflect.Struct:
|
||||
s.Type = "object"
|
||||
// no additional properties are allowed
|
||||
s.AdditionalProperties = falseSchema()
|
||||
|
||||
// If skipPath is non-nil, it is path to an anonymous field whose
|
||||
// schema has been replaced by a known schema.
|
||||
var skipPath []int
|
||||
for _, field := range reflect.VisibleFields(t) {
|
||||
if s.Properties == nil {
|
||||
s.Properties = make(map[string]*Schema)
|
||||
}
|
||||
if field.Anonymous {
|
||||
override := schemas[field.Type]
|
||||
if override != nil {
|
||||
// Type must be object, and only properties can be set.
|
||||
if override.Type != "object" {
|
||||
return nil, fmt.Errorf(`custom schema for embedded struct must have type "object", got %q`,
|
||||
override.Type)
|
||||
}
|
||||
// Check that all keywords relevant for objects are absent, except properties.
|
||||
ov := reflect.ValueOf(override).Elem()
|
||||
for _, sfi := range schemaFieldInfos {
|
||||
if sfi.sf.Name == "Type" || sfi.sf.Name == "Properties" {
|
||||
continue
|
||||
}
|
||||
fv := ov.FieldByIndex(sfi.sf.Index)
|
||||
if !fv.IsZero() {
|
||||
return nil, fmt.Errorf(`overrides for embedded fields can have only "Type" and "Properties"; this has %q`, sfi.sf.Name)
|
||||
}
|
||||
}
|
||||
|
||||
skipPath = field.Index
|
||||
keys := make([]string, 0, len(override.Properties))
|
||||
for k := range override.Properties {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
for _, name := range keys {
|
||||
if _, ok := s.Properties[name]; !ok {
|
||||
s.Properties[name] = override.Properties[name].CloneSchemas()
|
||||
s.PropertyOrder = append(s.PropertyOrder, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check to see if this field has been promoted from a replaced anonymous
|
||||
// type.
|
||||
if skipPath != nil {
|
||||
skip := false
|
||||
if len(field.Index) >= len(skipPath) {
|
||||
skip = true
|
||||
for i, index := range skipPath {
|
||||
if field.Index[i] != index {
|
||||
// If we're no longer in a subfield.
|
||||
skip = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
} else {
|
||||
// Anonymous fields are followed immediately by their promoted fields.
|
||||
// Once we encounter a field that *isn't* promoted, we can stop
|
||||
// checking.
|
||||
skipPath = nil
|
||||
}
|
||||
}
|
||||
|
||||
info := fieldJSONInfo(field)
|
||||
if info.omit {
|
||||
continue
|
||||
}
|
||||
fs, err := forType(field.Type, seen, ignore, schemas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ignore && fs == nil {
|
||||
// Skip fields of invalid type.
|
||||
continue
|
||||
}
|
||||
if tag, ok := field.Tag.Lookup("jsonschema"); ok {
|
||||
if tag == "" {
|
||||
return nil, fmt.Errorf("empty jsonschema tag on struct field %s.%s", t, field.Name)
|
||||
}
|
||||
if disallowedPrefixRegexp.MatchString(tag) {
|
||||
return nil, fmt.Errorf("tag must not begin with 'WORD=': %q", tag)
|
||||
}
|
||||
fs.Description = tag
|
||||
}
|
||||
s.Properties[info.name] = fs
|
||||
|
||||
s.PropertyOrder = append(s.PropertyOrder, info.name)
|
||||
|
||||
if !info.settings["omitempty"] && !info.settings["omitzero"] {
|
||||
s.Required = append(s.Required, info.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove PropertyOrder duplicates, keeping the last occurrence
|
||||
if len(s.PropertyOrder) > 1 {
|
||||
seen := make(map[string]bool)
|
||||
// Create a slice to hold the cleaned order (capacity = current length)
|
||||
cleaned := make([]string, 0, len(s.PropertyOrder))
|
||||
|
||||
// Iterate backwards
|
||||
for i := len(s.PropertyOrder) - 1; i >= 0; i-- {
|
||||
name := s.PropertyOrder[i]
|
||||
if !seen[name] {
|
||||
cleaned = append(cleaned, name)
|
||||
seen[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Since we collected them backwards, we need to reverse the result
|
||||
// to restore the correct order.
|
||||
slices.Reverse(cleaned)
|
||||
s.PropertyOrder = cleaned
|
||||
}
|
||||
|
||||
default:
|
||||
if ignore {
|
||||
// Ignore.
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("type %v is unsupported by jsonschema", t)
|
||||
}
|
||||
if allowNull && s.Type != "" {
|
||||
s.Types = []string{"null", s.Type}
|
||||
s.Type = ""
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// initialSchemaMap holds types from the standard library that have MarshalJSON methods.
|
||||
var initialSchemaMap = make(map[reflect.Type]*Schema)
|
||||
|
||||
func init() {
|
||||
ss := &Schema{Type: "string"}
|
||||
initialSchemaMap[reflect.TypeFor[time.Time]()] = ss
|
||||
initialSchemaMap[reflect.TypeFor[slog.Level]()] = ss
|
||||
if os.Getenv(debugEnv) == "typeschemasnull=1" {
|
||||
initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
|
||||
} else {
|
||||
initialSchemaMap[reflect.TypeFor[big.Int]()] = ss
|
||||
}
|
||||
initialSchemaMap[reflect.TypeFor[big.Rat]()] = ss
|
||||
initialSchemaMap[reflect.TypeFor[big.Float]()] = ss
|
||||
}
|
||||
|
||||
// Disallow jsonschema tag values beginning "WORD=", for future expansion.
|
||||
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file implements JSON Pointers.
|
||||
// A JSON Pointer is a path that refers to one JSON value within another.
|
||||
// If the path is empty, it refers to the root value.
|
||||
// Otherwise, it is a sequence of slash-prefixed strings, like "/points/1/x",
|
||||
// selecting successive properties (for JSON objects) or items (for JSON arrays).
|
||||
// For example, when applied to this JSON value:
|
||||
// {
|
||||
// "points": [
|
||||
// {"x": 1, "y": 2},
|
||||
// {"x": 3, "y": 4}
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// the JSON Pointer "/points/1/x" refers to the number 3.
|
||||
// See the spec at https://datatracker.ietf.org/doc/html/rfc6901.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
jsonPointerEscaper = strings.NewReplacer("~", "~0", "/", "~1")
|
||||
jsonPointerUnescaper = strings.NewReplacer("~0", "~", "~1", "/")
|
||||
)
|
||||
|
||||
func escapeJSONPointerSegment(s string) string {
|
||||
return jsonPointerEscaper.Replace(s)
|
||||
}
|
||||
|
||||
func unescapeJSONPointerSegment(s string) string {
|
||||
return jsonPointerUnescaper.Replace(s)
|
||||
}
|
||||
|
||||
// parseJSONPointer splits a JSON Pointer into a sequence of segments. It doesn't
|
||||
// convert strings to numbers, because that depends on the traversal: a segment
|
||||
// is treated as a number when applied to an array, but a string when applied to
|
||||
// an object. See section 4 of the spec.
|
||||
func parseJSONPointer(ptr string) (segments []string, err error) {
|
||||
if ptr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if ptr[0] != '/' {
|
||||
return nil, fmt.Errorf("JSON Pointer %q does not begin with '/'", ptr)
|
||||
}
|
||||
// Unlike file paths, consecutive slashes are not coalesced.
|
||||
// Split is nicer than Cut here, because it gets a final "/" right.
|
||||
segments = strings.Split(ptr[1:], "/")
|
||||
if strings.Contains(ptr, "~") {
|
||||
// Undo the simple escaping rules that allow one to include a slash in a segment.
|
||||
for i := range segments {
|
||||
segments[i] = unescapeJSONPointerSegment(segments[i])
|
||||
}
|
||||
}
|
||||
return segments, nil
|
||||
}
|
||||
|
||||
// dereferenceJSONPointer returns the Schema that sptr points to within s,
|
||||
// or an error if none.
|
||||
// This implementation suffices for JSON Schema: pointers are applied only to Schemas,
|
||||
// and refer only to Schemas.
|
||||
func dereferenceJSONPointer(s *Schema, sptr string) (_ *Schema, err error) {
|
||||
defer wrapf(&err, "JSON Pointer %q", sptr)
|
||||
|
||||
segments, err := parseJSONPointer(sptr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := reflect.ValueOf(s)
|
||||
for _, seg := range segments {
|
||||
switch v.Kind() {
|
||||
case reflect.Pointer:
|
||||
v = v.Elem()
|
||||
if !v.IsValid() {
|
||||
return nil, errors.New("navigated to nil reference")
|
||||
}
|
||||
fallthrough // if valid, can only be a pointer to a Schema
|
||||
|
||||
case reflect.Struct:
|
||||
// The segment must refer to a field in a Schema.
|
||||
if v.Type() != reflect.TypeFor[Schema]() {
|
||||
return nil, fmt.Errorf("navigated to non-Schema %s", v.Type())
|
||||
}
|
||||
v = lookupSchemaField(v, seg)
|
||||
if !v.IsValid() {
|
||||
return nil, fmt.Errorf("no schema field %q", seg)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
// The segment must be an integer without leading zeroes that refers to an item in the
|
||||
// slice or array.
|
||||
if seg == "-" {
|
||||
return nil, errors.New("the JSON Pointer array segment '-' is not supported")
|
||||
}
|
||||
if len(seg) > 1 && seg[0] == '0' {
|
||||
return nil, fmt.Errorf("segment %q has leading zeroes", seg)
|
||||
}
|
||||
n, err := strconv.Atoi(seg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid int: %q", seg)
|
||||
}
|
||||
if n < 0 || n >= v.Len() {
|
||||
return nil, fmt.Errorf("index %d is out of bounds for array of length %d", n, v.Len())
|
||||
}
|
||||
v = v.Index(n)
|
||||
// Cannot be invalid.
|
||||
case reflect.Map:
|
||||
// The segment must be a key in the map.
|
||||
v = v.MapIndex(reflect.ValueOf(seg))
|
||||
if !v.IsValid() {
|
||||
return nil, fmt.Errorf("no key %q in map", seg)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("value %s (%s) is not a schema, slice or map", v, v.Type())
|
||||
}
|
||||
}
|
||||
if s, ok := v.Interface().(*Schema); ok {
|
||||
return s, nil
|
||||
}
|
||||
return nil, fmt.Errorf("does not refer to a schema, but to a %s", v.Type())
|
||||
}
|
||||
|
||||
// lookupSchemaField returns the value of the field with the given name in v,
|
||||
// or the zero value if there is no such field or it is not of type Schema or *Schema.
|
||||
func lookupSchemaField(v reflect.Value, name string) reflect.Value {
|
||||
if name == "type" {
|
||||
// The "type" keyword may refer to Type or Types.
|
||||
// At most one will be non-zero.
|
||||
if t := v.FieldByName("Type"); !t.IsZero() {
|
||||
return t
|
||||
}
|
||||
return v.FieldByName("Types")
|
||||
}
|
||||
if name == "items" {
|
||||
// The "items" keyword refers to the "union type" that is either a schema or a schema array.
|
||||
// Implemented using the Items representing the schema and ItemsArray for the schema array.
|
||||
if items := v.FieldByName("Items"); items.IsValid() && !items.IsNil() {
|
||||
return items
|
||||
}
|
||||
return v.FieldByName("ItemsArray")
|
||||
}
|
||||
if name == "dependencies" {
|
||||
// The "dependencies" keyword refers to both DependencyStrings and DependencySchemas maps.
|
||||
// The value on schemaFieldMap is not garanteed to be DependencySchemas which we want
|
||||
// for pointer dereference. So we use FieldByName to get the DependencySchemas map.
|
||||
return v.FieldByName("DependencySchemas")
|
||||
}
|
||||
if sf, ok := schemaFieldMap[name]; ok {
|
||||
return v.FieldByIndex(sf.Index)
|
||||
}
|
||||
return reflect.Value{}
|
||||
}
|
||||
+589
@@ -0,0 +1,589 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file deals with preparing a schema for validation, including various checks,
|
||||
// optimizations, and the resolution of cross-schema references.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Resolved consists of a [Schema] along with associated information needed to
|
||||
// validate documents against it.
|
||||
// A Resolved has been validated against its meta-schema, and all its references
|
||||
// (the $ref and $dynamicRef keywords) have been resolved to their referenced Schemas.
|
||||
// Call [Schema.Resolve] to obtain a Resolved from a Schema.
|
||||
type Resolved struct {
|
||||
root *Schema
|
||||
draft draft
|
||||
// map from $ids to their schemas
|
||||
resolvedURIs map[string]*Schema
|
||||
// map from schemas to additional info computed during resolution
|
||||
resolvedInfos map[*Schema]*resolvedInfo
|
||||
}
|
||||
|
||||
type draft int
|
||||
|
||||
const (
|
||||
draft7 = iota
|
||||
draft2020
|
||||
)
|
||||
|
||||
func newResolved(s *Schema) *Resolved {
|
||||
return &Resolved{
|
||||
root: s,
|
||||
draft: detectDraft(s),
|
||||
resolvedURIs: map[string]*Schema{},
|
||||
resolvedInfos: map[*Schema]*resolvedInfo{},
|
||||
}
|
||||
}
|
||||
|
||||
// detectDraft inspects the raw JSON to determine the schema version.
|
||||
func detectDraft(s *Schema) draft {
|
||||
// Check explicit $schema declaration
|
||||
switch s.Schema {
|
||||
case draft7SchemaVersion, draft7SecSchemaVersion:
|
||||
return draft7
|
||||
case draft202012SchemaVersion:
|
||||
return draft2020
|
||||
default:
|
||||
// If nothing matches default to the latest supported version.
|
||||
return draft2020
|
||||
}
|
||||
}
|
||||
|
||||
// resolvedInfo holds information specific to a schema that is computed by [Schema.Resolve].
|
||||
type resolvedInfo struct {
|
||||
s *Schema
|
||||
// The JSON Pointer path from the root schema to here.
|
||||
// Used in errors.
|
||||
path string
|
||||
// The schema's base schema.
|
||||
// If the schema is the root or has an ID, its base is itself.
|
||||
// Otherwise, its base is the innermost enclosing schema whose base
|
||||
// is itself.
|
||||
// Intuitively, a base schema is one that can be referred to with a
|
||||
// fragmentless URI.
|
||||
base *Schema
|
||||
// The URI for the schema, if it is the root or has an ID.
|
||||
// Otherwise nil.
|
||||
// Invariants:
|
||||
// s.base.uri != nil.
|
||||
// s.base == s <=> s.uri != nil
|
||||
uri *url.URL
|
||||
// The schema to which Ref refers.
|
||||
resolvedRef *Schema
|
||||
|
||||
// If the schema has a dynamic ref, exactly one of the next two fields
|
||||
// will be non-zero after successful resolution.
|
||||
// The schema to which the dynamic ref refers when it acts lexically.
|
||||
resolvedDynamicRef *Schema
|
||||
// The anchor to look up on the stack when the dynamic ref acts dynamically.
|
||||
dynamicRefAnchor string
|
||||
|
||||
// The following fields are independent of arguments to Schema.Resolved,
|
||||
// so they could live on the Schema. We put them here for simplicity.
|
||||
|
||||
// The set of required properties.
|
||||
isRequired map[string]bool
|
||||
|
||||
// Compiled regexps.
|
||||
pattern *regexp.Regexp
|
||||
patternProperties map[*regexp.Regexp]*Schema
|
||||
|
||||
// Map from anchors to subschemas.
|
||||
anchors map[string]anchorInfo
|
||||
}
|
||||
|
||||
// Schema returns the schema that was resolved.
|
||||
// It must not be modified.
|
||||
func (r *Resolved) Schema() *Schema { return r.root }
|
||||
|
||||
// schemaString returns a short string describing the schema.
|
||||
func (r *Resolved) schemaString(s *Schema) string {
|
||||
if s.ID != "" {
|
||||
return s.ID
|
||||
}
|
||||
info := r.resolvedInfos[s]
|
||||
if info.path != "" {
|
||||
return info.path
|
||||
}
|
||||
return "<anonymous schema>"
|
||||
}
|
||||
|
||||
// A Loader reads and unmarshals the schema at uri, if any.
|
||||
type Loader func(uri *url.URL) (*Schema, error)
|
||||
|
||||
// ResolveOptions are options for [Schema.Resolve].
|
||||
type ResolveOptions struct {
|
||||
// BaseURI is the URI relative to which the root schema should be resolved.
|
||||
// If non-empty, must be an absolute URI (one that starts with a scheme).
|
||||
// It is resolved (in the URI sense; see [url.ResolveReference]) with root's
|
||||
// $id property.
|
||||
// If the resulting URI is not absolute, then the schema cannot contain
|
||||
// relative URI references.
|
||||
BaseURI string
|
||||
// Loader loads schemas that are referred to by a $ref but are not under the
|
||||
// root schema (remote references).
|
||||
// If nil, resolving a remote reference will return an error.
|
||||
Loader Loader
|
||||
// ValidateDefaults determines whether to validate values of "default" keywords
|
||||
// against their schemas.
|
||||
// The [JSON Schema specification] does not require this, but it is recommended
|
||||
// if defaults will be used.
|
||||
//
|
||||
// [JSON Schema specification]: https://json-schema.org/understanding-json-schema/reference/annotations
|
||||
ValidateDefaults bool
|
||||
}
|
||||
|
||||
// Resolve resolves all references within the schema and performs other tasks that
|
||||
// prepare the schema for validation.
|
||||
// If opts is nil, the default values are used.
|
||||
// The schema must not be changed after Resolve is called.
|
||||
// The same schema may be resolved multiple times.
|
||||
func (root *Schema) Resolve(opts *ResolveOptions) (*Resolved, error) {
|
||||
// There are up to five steps required to prepare a schema to validate.
|
||||
// 1. Load: read the schema from somewhere and unmarshal it.
|
||||
// This schema (root) may have been loaded or created in memory, but other schemas that
|
||||
// come into the picture in step 4 will be loaded by the given loader.
|
||||
// 2. Check: validate the schema against a meta-schema, and perform other well-formedness checks.
|
||||
// Precompute some values along the way.
|
||||
// 3. Resolve URIs: determine the base URI of the root and all its subschemas, and
|
||||
// resolve (in the URI sense) all identifiers and anchors with their bases. This step results
|
||||
// in a map from URIs to schemas within root.
|
||||
// 4. Resolve references: all refs in the schemas are replaced with the schema they refer to.
|
||||
// 5. (Optional.) If opts.ValidateDefaults is true, validate the defaults.
|
||||
r := &resolver{loaded: map[string]*Resolved{}}
|
||||
if opts != nil {
|
||||
r.opts = *opts
|
||||
}
|
||||
var base *url.URL
|
||||
if r.opts.BaseURI == "" {
|
||||
base = &url.URL{} // so we can call ResolveReference on it
|
||||
} else {
|
||||
var err error
|
||||
base, err = url.Parse(r.opts.BaseURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing base URI: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.opts.Loader == nil {
|
||||
r.opts.Loader = func(uri *url.URL) (*Schema, error) {
|
||||
return nil, errors.New("cannot resolve remote schemas: no loader passed to Schema.Resolve")
|
||||
}
|
||||
}
|
||||
|
||||
resolved, err := r.resolve(root, base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.opts.ValidateDefaults {
|
||||
if err := resolved.validateDefaults(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// TODO: before we return, throw away anything we don't need for validation.
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// A resolver holds the state for resolution.
|
||||
type resolver struct {
|
||||
opts ResolveOptions
|
||||
// A cache of loaded and partly resolved schemas. (They may not have had their
|
||||
// refs resolved.) The cache ensures that the loader will never be called more
|
||||
// than once with the same URI, and that reference cycles are handled properly.
|
||||
loaded map[string]*Resolved
|
||||
}
|
||||
|
||||
func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
|
||||
if baseURI.Fragment != "" {
|
||||
return nil, fmt.Errorf("base URI %s must not have a fragment", baseURI)
|
||||
}
|
||||
rs := newResolved(s)
|
||||
|
||||
if err := s.check(rs.resolvedInfos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := resolveURIs(rs, baseURI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remember the schema by both the URI we loaded it from and its canonical name,
|
||||
// which may differ if the schema has an $id.
|
||||
// We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
|
||||
r.loaded[baseURI.String()] = rs
|
||||
r.loaded[rs.resolvedInfos[s].uri.String()] = rs
|
||||
|
||||
if err := r.resolveRefs(rs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (root *Schema) check(infos map[*Schema]*resolvedInfo) error {
|
||||
// Check for structural validity. Do this first and fail fast:
|
||||
// bad structure will cause other code to panic.
|
||||
if err := root.checkStructure(infos); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
report := func(err error) { errs = append(errs, err) }
|
||||
|
||||
for ss := range root.all() {
|
||||
ss.checkLocal(report, infos)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// checkStructure verifies that root and its subschemas form a tree.
|
||||
// It also assigns each schema a unique path, to improve error messages.
|
||||
func (root *Schema) checkStructure(infos map[*Schema]*resolvedInfo) error {
|
||||
assert(len(infos) == 0, "non-empty infos")
|
||||
|
||||
var check func(reflect.Value, []byte) error
|
||||
check = func(v reflect.Value, path []byte) error {
|
||||
// For the purpose of error messages, the root schema has path "root"
|
||||
// and other schemas' paths are their JSON Pointer from the root.
|
||||
p := "root"
|
||||
if len(path) > 0 {
|
||||
p = string(path)
|
||||
}
|
||||
s := v.Interface().(*Schema)
|
||||
if s == nil {
|
||||
return fmt.Errorf("jsonschema: schema at %s is nil", p)
|
||||
}
|
||||
if info, ok := infos[s]; ok {
|
||||
// We've seen s before.
|
||||
// The schema graph at root is not a tree, but it needs to
|
||||
// be because a schema's base must be unique.
|
||||
// A cycle would also put Schema.all into an infinite recursion.
|
||||
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
|
||||
root, info.path, p)
|
||||
}
|
||||
infos[s] = &resolvedInfo{s: s, path: p}
|
||||
|
||||
for _, info := range schemaFieldInfos {
|
||||
fv := v.Elem().FieldByIndex(info.sf.Index)
|
||||
switch info.sf.Type {
|
||||
case schemaType:
|
||||
// A field that contains an individual schema.
|
||||
// A nil is valid: it just means the field isn't present.
|
||||
if !fv.IsNil() {
|
||||
if err := check(fv, fmt.Appendf(path, "/%s", info.jsonName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case schemaSliceType:
|
||||
for i := range fv.Len() {
|
||||
if err := check(fv.Index(i), fmt.Appendf(path, "/%s/%d", info.jsonName, i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case schemaMapType:
|
||||
iter := fv.MapRange()
|
||||
for iter.Next() {
|
||||
key := escapeJSONPointerSegment(iter.Key().String())
|
||||
if err := check(iter.Value(), fmt.Appendf(path, "/%s/%s", info.jsonName, key)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return check(reflect.ValueOf(root), make([]byte, 0, 256))
|
||||
}
|
||||
|
||||
// checkLocal checks s for validity, independently of other schemas it may refer to.
|
||||
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
|
||||
// in the schema for later use.
|
||||
// It appends the errors it finds to errs.
|
||||
func (s *Schema) checkLocal(report func(error), infos map[*Schema]*resolvedInfo) {
|
||||
addf := func(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
report(fmt.Errorf("jsonschema.Schema: %s: %s", s, msg))
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
addf("nil subschema")
|
||||
return
|
||||
}
|
||||
if err := s.basicChecks(); err != nil {
|
||||
report(err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: validate the schema's properties,
|
||||
// ideally by jsonschema-validating it against the meta-schema.
|
||||
|
||||
// Some properties are present so that Schemas can round-trip, but we do not
|
||||
// validate them.
|
||||
// Currently, it's just the $vocabulary property.
|
||||
// As a special case, we can validate the 2020-12 meta-schema.
|
||||
if s.Vocabulary != nil && s.Schema != draft202012SchemaVersion {
|
||||
addf("cannot validate a schema with $vocabulary")
|
||||
}
|
||||
|
||||
info := infos[s]
|
||||
|
||||
// Check and compile regexps.
|
||||
if s.Pattern != "" {
|
||||
re, err := regexp.Compile(s.Pattern)
|
||||
if err != nil {
|
||||
addf("pattern: %v", err)
|
||||
} else {
|
||||
info.pattern = re
|
||||
}
|
||||
}
|
||||
if len(s.PatternProperties) > 0 {
|
||||
info.patternProperties = map[*regexp.Regexp]*Schema{}
|
||||
for reString, subschema := range s.PatternProperties {
|
||||
re, err := regexp.Compile(reString)
|
||||
if err != nil {
|
||||
addf("patternProperties[%q]: %v", reString, err)
|
||||
continue
|
||||
}
|
||||
info.patternProperties[re] = subschema
|
||||
}
|
||||
}
|
||||
|
||||
// Build a set of required properties, to avoid quadratic behavior when validating
|
||||
// a struct.
|
||||
if len(s.Required) > 0 {
|
||||
info.isRequired = map[string]bool{}
|
||||
for _, r := range s.Required {
|
||||
info.isRequired[r] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolveURIs resolves the ids and anchors in all the schemas of root, relative
|
||||
// to baseURI.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
|
||||
// 8.2.1.
|
||||
//
|
||||
// Every schema has a base URI and a parent base URI.
|
||||
//
|
||||
// The parent base URI is the base URI of the lexically enclosing schema, or for
|
||||
// a root schema, the URI it was loaded from or the one supplied to [Schema.Resolve].
|
||||
//
|
||||
// If the schema has no $id property, the base URI of a schema is that of its parent.
|
||||
// If the schema does have an $id, it must be a URI, possibly relative. The schema's
|
||||
// base URI is the $id resolved (in the sense of [url.URL.ResolveReference]) against
|
||||
// the parent base.
|
||||
//
|
||||
// As an example, consider this schema loaded from http://a.com/root.json (quotes omitted):
|
||||
//
|
||||
// {
|
||||
// allOf: [
|
||||
// {$id: "sub1.json", minLength: 5},
|
||||
// {$id: "http://b.com", minimum: 10},
|
||||
// {not: {maximum: 20}}
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// The base URIs are as follows. Schema locations are expressed in the JSON Pointer notation.
|
||||
//
|
||||
// schema base URI
|
||||
// root http://a.com/root.json
|
||||
// allOf/0 http://a.com/sub1.json
|
||||
// allOf/1 http://b.com (absolute $id; doesn't matter that it's not under the loaded URI)
|
||||
// allOf/2 http://a.com/root.json (inherited from parent)
|
||||
// allOf/2/not http://a.com/root.json (inherited from parent)
|
||||
func resolveURIs(rs *Resolved, baseURI *url.URL) error {
|
||||
// Anchors and dynamic anchors are URI fragments that are scoped to their base.
|
||||
// We treat them as keys in a map stored within the schema.
|
||||
setAnchor := func(s *Schema, baseInfo *resolvedInfo, anchor string, dynamic bool) error {
|
||||
if anchor != "" {
|
||||
if _, ok := baseInfo.anchors[anchor]; ok {
|
||||
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseInfo.uri)
|
||||
}
|
||||
if baseInfo.anchors == nil {
|
||||
baseInfo.anchors = map[string]anchorInfo{}
|
||||
}
|
||||
baseInfo.anchors[anchor] = anchorInfo{s, dynamic}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var resolve func(s, base *Schema) error
|
||||
resolve = func(s, base *Schema) error {
|
||||
info := rs.resolvedInfos[s]
|
||||
baseInfo := rs.resolvedInfos[base]
|
||||
|
||||
// ids are scoped to the root.
|
||||
if s.ID != "" {
|
||||
// draft-7 specific
|
||||
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3
|
||||
// "All other properties in a "$ref" object MUST be ignored."
|
||||
ignore := rs.draft == draft7 && s.Ref != ""
|
||||
if !ignore {
|
||||
// A non-empty ID establishes a new base.
|
||||
idURI, err := url.Parse(s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rs.draft == draft2020 && idURI.Fragment != "" {
|
||||
return fmt.Errorf("$id %s must not have a fragment", s.ID)
|
||||
}
|
||||
if rs.draft == draft7 && idURI.Fragment != "" {
|
||||
// anchor did not exist in draft 7, id was used for base uri and document navigation
|
||||
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#id-keyword
|
||||
anchorName := strings.TrimPrefix(s.ID, "#")
|
||||
setAnchor(s, baseInfo, anchorName, false)
|
||||
} else {
|
||||
// The base URI for this schema is its $id resolved against the parent base.
|
||||
info.uri = baseInfo.uri.ResolveReference(idURI)
|
||||
if !info.uri.IsAbs() {
|
||||
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %q)", s.ID, baseInfo.uri)
|
||||
}
|
||||
rs.resolvedURIs[info.uri.String()] = s
|
||||
base = s // needed for anchors
|
||||
baseInfo = rs.resolvedInfos[base]
|
||||
}
|
||||
}
|
||||
}
|
||||
info.base = base
|
||||
if rs.draft == draft2020 {
|
||||
setAnchor(s, baseInfo, s.Anchor, false)
|
||||
setAnchor(s, baseInfo, s.DynamicAnchor, true)
|
||||
}
|
||||
|
||||
for c := range s.children() {
|
||||
if err := resolve(c, base); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the root URI to the base for now. If the root has an $id, this will change.
|
||||
rs.resolvedInfos[rs.root].uri = baseURI
|
||||
// The original base, even if changed, is still a valid way to refer to the root.
|
||||
rs.resolvedURIs[baseURI.String()] = rs.root
|
||||
|
||||
return resolve(rs.root, rs.root)
|
||||
}
|
||||
|
||||
// resolveRefs replaces every ref in the schemas with the schema it refers to.
|
||||
// A reference that doesn't resolve within the schema may refer to some other schema
|
||||
// that needs to be loaded.
|
||||
func (r *resolver) resolveRefs(rs *Resolved) error {
|
||||
for s := range rs.root.all() {
|
||||
info := rs.resolvedInfos[s]
|
||||
if s.Ref != "" {
|
||||
refSchema, _, err := r.resolveRef(rs, s, s.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Whether or not the anchor referred to by $ref fragment is dynamic,
|
||||
// the ref still treats it lexically.
|
||||
info.resolvedRef = refSchema
|
||||
}
|
||||
if s.DynamicRef != "" {
|
||||
refSchema, frag, err := r.resolveRef(rs, s, s.DynamicRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if frag != "" {
|
||||
// The dynamic ref's fragment points to a dynamic anchor.
|
||||
// We must resolve the fragment at validation time.
|
||||
info.dynamicRefAnchor = frag
|
||||
} else {
|
||||
// There is no dynamic anchor in the lexically referenced schema,
|
||||
// so the dynamic ref behaves like a lexical ref.
|
||||
info.resolvedDynamicRef = refSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveRef resolves the reference ref, which is either s.Ref or s.DynamicRef.
|
||||
func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, dynamicFragment string, err error) {
|
||||
refURI, err := url.Parse(ref)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// URI-resolve the ref against the current base URI to get a complete URI.
|
||||
base := rs.resolvedInfos[s].base
|
||||
refURI = rs.resolvedInfos[base].uri.ResolveReference(refURI)
|
||||
// The non-fragment part of a ref URI refers to the base URI of some schema.
|
||||
// This part is the same for dynamic refs too: their non-fragment part resolves
|
||||
// lexically.
|
||||
u := *refURI
|
||||
u.Fragment = ""
|
||||
fraglessRefURI := &u
|
||||
// Look it up locally.
|
||||
referencedSchema := rs.resolvedURIs[fraglessRefURI.String()]
|
||||
if referencedSchema == nil {
|
||||
// The schema is remote. Maybe we've already loaded it.
|
||||
// We assume that the non-fragment part of refURI refers to a top-level schema
|
||||
// document. That is, we don't support the case exemplified by
|
||||
// http://foo.com/bar.json/baz, where the document is in bar.json and
|
||||
// the reference points to a subschema within it.
|
||||
// TODO: support that case.
|
||||
if lrs := r.loaded[fraglessRefURI.String()]; lrs != nil {
|
||||
referencedSchema = lrs.root
|
||||
} else {
|
||||
// Try to load the schema.
|
||||
ls, err := r.opts.Loader(fraglessRefURI)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("loading %s: %w", fraglessRefURI, err)
|
||||
}
|
||||
// Check if referenced schema has $schema defined. If not it should inherit the resolved
|
||||
if ls.Schema == "" {
|
||||
ls.Schema = s.Schema
|
||||
}
|
||||
lrs, err := r.resolve(ls, fraglessRefURI)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
referencedSchema = lrs.root
|
||||
assert(referencedSchema != nil, "nil referenced schema")
|
||||
// Copy the resolvedInfos from lrs into rs, without overwriting
|
||||
// (hence we can't use maps.Insert).
|
||||
for s, i := range lrs.resolvedInfos {
|
||||
if rs.resolvedInfos[s] == nil {
|
||||
rs.resolvedInfos[s] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frag := refURI.Fragment
|
||||
// Look up frag in refSchema.
|
||||
// frag is either a JSON Pointer or the name of an anchor.
|
||||
// A JSON Pointer is either the empty string or begins with a '/',
|
||||
// whereas anchors are always non-empty strings that don't contain slashes.
|
||||
if frag != "" && !strings.HasPrefix(frag, "/") {
|
||||
resInfo := rs.resolvedInfos[referencedSchema]
|
||||
info, found := resInfo.anchors[frag]
|
||||
|
||||
if !found {
|
||||
return nil, "", fmt.Errorf("no anchor %q in %s", frag, s)
|
||||
}
|
||||
if info.dynamic {
|
||||
dynamicFragment = frag
|
||||
}
|
||||
return info.schema, dynamicFragment, nil
|
||||
}
|
||||
// frag is a JSON Pointer.
|
||||
s, err = dereferenceJSONPointer(referencedSchema, frag)
|
||||
return s, "", err
|
||||
}
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// A Schema is a JSON schema object.
|
||||
// It supports both draft-07 and the 2020-12 draft specifications:
|
||||
// - Draft-07: https://json-schema.org/draft-07/draft-handrews-json-schema-01
|
||||
// and https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01
|
||||
// - 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.
|
||||
// There is one exception to provide more Go type-safety: the Type and Types fields
|
||||
// are mutually exclusive.
|
||||
//
|
||||
// Since this struct is a Go representation of a JSON value, it inherits JSON's
|
||||
// distinction between nil and empty. Nil slices and maps are considered absent,
|
||||
// but empty ones are present and affect validation. For example,
|
||||
//
|
||||
// Schema{Enum: nil}
|
||||
//
|
||||
// is equivalent to an empty schema, so it validates every instance. But
|
||||
//
|
||||
// Schema{Enum: []any{}}
|
||||
//
|
||||
// requires equality to some slice element, so it vacuously rejects every instance.
|
||||
type Schema struct {
|
||||
// core
|
||||
ID string `json:"$id,omitempty"`
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Ref string `json:"$ref,omitempty"`
|
||||
Comment string `json:"$comment,omitempty"`
|
||||
Defs map[string]*Schema `json:"$defs,omitempty"`
|
||||
Definitions map[string]*Schema `json:"definitions,omitempty"`
|
||||
|
||||
// split draft 7 Dependencies into DependencySchemas and DependencyStrings
|
||||
DependencySchemas map[string]*Schema `json:"-"`
|
||||
DependencyStrings map[string][]string `json:"-"`
|
||||
|
||||
Anchor string `json:"$anchor,omitempty"`
|
||||
DynamicAnchor string `json:"$dynamicAnchor,omitempty"`
|
||||
DynamicRef string `json:"$dynamicRef,omitempty"`
|
||||
Vocabulary map[string]bool `json:"$vocabulary,omitempty"`
|
||||
|
||||
// metadata
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Default json.RawMessage `json:"default,omitempty"`
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
WriteOnly bool `json:"writeOnly,omitempty"`
|
||||
Examples []any `json:"examples,omitempty"`
|
||||
|
||||
// validation
|
||||
// Use Type for a single type, or Types for multiple types; never both.
|
||||
Type string `json:"-"`
|
||||
Types []string `json:"-"`
|
||||
Enum []any `json:"enum,omitempty"`
|
||||
// Const is *any because a JSON null (Go nil) is a valid value.
|
||||
Const *any `json:"const,omitempty"`
|
||||
MultipleOf *float64 `json:"multipleOf,omitempty"`
|
||||
Minimum *float64 `json:"minimum,omitempty"`
|
||||
Maximum *float64 `json:"maximum,omitempty"`
|
||||
ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
|
||||
ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
|
||||
MinLength *int `json:"minLength,omitempty"`
|
||||
MaxLength *int `json:"maxLength,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
|
||||
// arrays
|
||||
PrefixItems []*Schema `json:"prefixItems,omitempty"`
|
||||
Items *Schema `json:"-"`
|
||||
ItemsArray []*Schema `json:"-"`
|
||||
MinItems *int `json:"minItems,omitempty"`
|
||||
MaxItems *int `json:"maxItems,omitempty"`
|
||||
AdditionalItems *Schema `json:"additionalItems,omitempty"`
|
||||
UniqueItems bool `json:"uniqueItems,omitempty"`
|
||||
Contains *Schema `json:"contains,omitempty"`
|
||||
MinContains *int `json:"minContains,omitempty"` // *int, not int: default is 1, not 0
|
||||
MaxContains *int `json:"maxContains,omitempty"`
|
||||
UnevaluatedItems *Schema `json:"unevaluatedItems,omitempty"`
|
||||
|
||||
// objects
|
||||
MinProperties *int `json:"minProperties,omitempty"`
|
||||
MaxProperties *int `json:"maxProperties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
DependentRequired map[string][]string `json:"dependentRequired,omitempty"`
|
||||
Properties map[string]*Schema `json:"properties,omitempty"`
|
||||
PatternProperties map[string]*Schema `json:"patternProperties,omitempty"`
|
||||
AdditionalProperties *Schema `json:"additionalProperties,omitempty"`
|
||||
PropertyNames *Schema `json:"propertyNames,omitempty"`
|
||||
UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"`
|
||||
|
||||
// logic
|
||||
AllOf []*Schema `json:"allOf,omitempty"`
|
||||
AnyOf []*Schema `json:"anyOf,omitempty"`
|
||||
OneOf []*Schema `json:"oneOf,omitempty"`
|
||||
Not *Schema `json:"not,omitempty"`
|
||||
|
||||
// conditional
|
||||
If *Schema `json:"if,omitempty"`
|
||||
Then *Schema `json:"then,omitempty"`
|
||||
Else *Schema `json:"else,omitempty"`
|
||||
DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"`
|
||||
|
||||
// other
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
|
||||
ContentEncoding string `json:"contentEncoding,omitempty"`
|
||||
ContentMediaType string `json:"contentMediaType,omitempty"`
|
||||
ContentSchema *Schema `json:"contentSchema,omitempty"`
|
||||
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
|
||||
Format string `json:"format,omitempty"`
|
||||
|
||||
// Extra allows for additional keywords beyond those specified.
|
||||
Extra map[string]any `json:"-"`
|
||||
|
||||
// PropertyOrder records the ordering of properties for JSON rendering.
|
||||
//
|
||||
// During [For], PropertyOrder is set to the field order,
|
||||
// if the type used for inference is a struct.
|
||||
//
|
||||
// If PropertyOrder is set, it controls the relative ordering of properties in [Schema.MarshalJSON].
|
||||
// The rendered JSON first lists any properties that appear in the PropertyOrder slice in the order
|
||||
// they appear, followed by all other properties that do not appear in the PropertyOrder slice in an
|
||||
// undefined but deterministic order.
|
||||
PropertyOrder []string `json:"-"`
|
||||
}
|
||||
|
||||
// falseSchema returns a new Schema tree that fails to validate any value.
|
||||
func falseSchema() *Schema {
|
||||
return &Schema{Not: &Schema{}}
|
||||
}
|
||||
|
||||
// anchorInfo records the subschema to which an anchor refers, and whether
|
||||
// the anchor keyword is $anchor or $dynamicAnchor.
|
||||
type anchorInfo struct {
|
||||
schema *Schema
|
||||
dynamic bool
|
||||
}
|
||||
|
||||
// String returns a short description of the schema.
|
||||
func (s *Schema) String() string {
|
||||
if s.ID != "" {
|
||||
return s.ID
|
||||
}
|
||||
if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" {
|
||||
return fmt.Sprintf("anchor %s", a)
|
||||
}
|
||||
return "<anonymous schema>"
|
||||
}
|
||||
|
||||
// CloneSchemas returns a copy of s.
|
||||
// The copy is shallow except for sub-schemas, which are themelves copied with CloneSchemas.
|
||||
// This allows both s and s.CloneSchemas() to appear as sub-schemas of the same parent.
|
||||
func (s *Schema) CloneSchemas() *Schema {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s2 := *s
|
||||
v := reflect.ValueOf(&s2)
|
||||
for _, info := range schemaFieldInfos {
|
||||
fv := v.Elem().FieldByIndex(info.sf.Index)
|
||||
switch info.sf.Type {
|
||||
case schemaType:
|
||||
sscss := fv.Interface().(*Schema)
|
||||
fv.Set(reflect.ValueOf(sscss.CloneSchemas()))
|
||||
|
||||
case schemaSliceType:
|
||||
slice := fv.Interface().([]*Schema)
|
||||
slice = slices.Clone(slice)
|
||||
for i, ss := range slice {
|
||||
slice[i] = ss.CloneSchemas()
|
||||
}
|
||||
fv.Set(reflect.ValueOf(slice))
|
||||
|
||||
case schemaMapType:
|
||||
m := fv.Interface().(map[string]*Schema)
|
||||
m = maps.Clone(m)
|
||||
for k, ss := range m {
|
||||
m[k] = ss.CloneSchemas()
|
||||
}
|
||||
fv.Set(reflect.ValueOf(m))
|
||||
|
||||
}
|
||||
}
|
||||
return &s2
|
||||
}
|
||||
|
||||
func (s *Schema) basicChecks() error {
|
||||
if s.Type != "" && s.Types != nil {
|
||||
return errors.New("both Type and Types are set; at most one should be")
|
||||
}
|
||||
if s.Defs != nil && s.Definitions != nil {
|
||||
return errors.New("both Defs and Definitions are set; at most one should be")
|
||||
}
|
||||
if s.Items != nil && s.ItemsArray != nil {
|
||||
return errors.New("both Items and ItemsArray are set; at most one should be")
|
||||
}
|
||||
propertyOrderSeen := make(map[string]bool)
|
||||
for _, val := range s.PropertyOrder {
|
||||
if _, ok := propertyOrderSeen[val]; ok {
|
||||
// Duplicate found
|
||||
return fmt.Errorf("property order slice cannot contain duplicate entries, found duplicate %q", val)
|
||||
}
|
||||
propertyOrderSeen[val] = true
|
||||
}
|
||||
|
||||
for key := range s.DependencySchemas {
|
||||
// Check if the key exists in the dependency strings map
|
||||
if _, exists := s.DependencyStrings[key]; exists {
|
||||
return fmt.Errorf("dependency key %q cannot be defined as both a schema and a string array", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type schemaWithoutMethods Schema // doesn't implement json.{Unm,M}arshaler
|
||||
|
||||
func (s Schema) MarshalJSON() ([]byte, error) {
|
||||
// NOTE: Use a value receiver here to avoid the encoding/json bugs
|
||||
// described in golang/go#22967, golang/go#33993, and golang/go#55890.
|
||||
// With a pointer receiver, MarshalJSON is only called for Schema in
|
||||
// some cases (for example when the field value is addressable, or not
|
||||
// stored as a map value), which leads to inconsistent JSON encoding.
|
||||
// A value receiver makes Schema itself implement json.Marshaler and
|
||||
// ensures that encoding/json always calls this method.
|
||||
if err := s.basicChecks(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Marshal either Type or Types as "type".
|
||||
var typ any
|
||||
switch {
|
||||
case s.Type != "":
|
||||
typ = s.Type
|
||||
case s.Types != nil:
|
||||
typ = s.Types
|
||||
}
|
||||
|
||||
var items any
|
||||
switch {
|
||||
case s.Items != nil:
|
||||
items = s.Items
|
||||
case s.ItemsArray != nil:
|
||||
items = s.ItemsArray
|
||||
}
|
||||
|
||||
var dep map[string]any
|
||||
size := len(s.DependencySchemas) + len(s.DependencyStrings)
|
||||
if size > 0 {
|
||||
dep = make(map[string]any, size)
|
||||
for k, v := range s.DependencySchemas {
|
||||
dep[k] = v
|
||||
}
|
||||
for k, v := range s.DependencyStrings {
|
||||
dep[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
ms := struct {
|
||||
Type any `json:"type,omitempty"`
|
||||
Properties json.Marshaler `json:"properties,omitempty"`
|
||||
Dependencies map[string]any `json:"dependencies,omitempty"`
|
||||
Items any `json:"items,omitempty"`
|
||||
*schemaWithoutMethods
|
||||
}{
|
||||
Type: typ,
|
||||
Dependencies: dep,
|
||||
Items: items,
|
||||
schemaWithoutMethods: (*schemaWithoutMethods)(&s),
|
||||
}
|
||||
// Marshal properties, even if the empty map (but not nil).
|
||||
if s.Properties != nil {
|
||||
ms.Properties = orderedProperties{
|
||||
props: s.Properties,
|
||||
order: s.PropertyOrder,
|
||||
}
|
||||
}
|
||||
|
||||
bs, err := marshalStructWithMap(&ms, "Extra")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Marshal {} as true and {"not": {}} as false.
|
||||
// It is wasteful to do this here instead of earlier, but much easier.
|
||||
switch {
|
||||
case bytes.Equal(bs, []byte(`{}`)):
|
||||
bs = []byte("true")
|
||||
case bytes.Equal(bs, []byte(`{"not":true}`)):
|
||||
bs = []byte("false")
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// orderedProperties is a helper to marshal the properties map in a specific order.
|
||||
type orderedProperties struct {
|
||||
props map[string]*Schema
|
||||
order []string
|
||||
}
|
||||
|
||||
func (op orderedProperties) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
|
||||
first := true
|
||||
processed := make(map[string]bool, len(op.props))
|
||||
|
||||
// Helper closure to write "key": value
|
||||
writeEntry := func(key string, val *Schema) error {
|
||||
if !first {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
first = false
|
||||
|
||||
// Marshal the Key
|
||||
keyBytes, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.Write(keyBytes)
|
||||
|
||||
buf.WriteByte(':')
|
||||
|
||||
// Marshal the Value
|
||||
valBytes, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.Write(valBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write keys explicitly listed in PropertyOrder
|
||||
for _, name := range op.order {
|
||||
if prop, ok := op.props[name]; ok {
|
||||
if err := writeEntry(name, prop); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processed[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Write any remaining keys
|
||||
var remaining []string
|
||||
for name := range op.props {
|
||||
if !processed[name] {
|
||||
remaining = append(remaining, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the slice alphabetically
|
||||
slices.Sort(remaining)
|
||||
|
||||
for _, name := range remaining {
|
||||
if err := writeEntry(name, op.props[name]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *Schema) UnmarshalJSON(data []byte) error {
|
||||
// A JSON boolean is a valid schema.
|
||||
var b bool
|
||||
if err := json.Unmarshal(data, &b); err == nil {
|
||||
if b {
|
||||
// true is the empty schema, which validates everything.
|
||||
*s = Schema{}
|
||||
} else {
|
||||
// false is the schema that validates nothing.
|
||||
*s = *falseSchema()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ms := struct {
|
||||
Type json.RawMessage `json:"type,omitempty"`
|
||||
Dependencies map[string]json.RawMessage `json:"dependencies,omitempty"`
|
||||
Items json.RawMessage `json:"items,omitempty"`
|
||||
Const json.RawMessage `json:"const,omitempty"`
|
||||
MinLength *integer `json:"minLength,omitempty"`
|
||||
MaxLength *integer `json:"maxLength,omitempty"`
|
||||
MinItems *integer `json:"minItems,omitempty"`
|
||||
MaxItems *integer `json:"maxItems,omitempty"`
|
||||
MinProperties *integer `json:"minProperties,omitempty"`
|
||||
MaxProperties *integer `json:"maxProperties,omitempty"`
|
||||
MinContains *integer `json:"minContains,omitempty"`
|
||||
MaxContains *integer `json:"maxContains,omitempty"`
|
||||
|
||||
*schemaWithoutMethods
|
||||
}{
|
||||
schemaWithoutMethods: (*schemaWithoutMethods)(s),
|
||||
}
|
||||
if err := unmarshalStructWithMap(data, &ms, "Extra"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Unmarshal "type" as either Type or Types.
|
||||
var err error
|
||||
if len(ms.Type) > 0 {
|
||||
switch ms.Type[0] {
|
||||
case '"':
|
||||
err = json.Unmarshal(ms.Type, &s.Type)
|
||||
case '[':
|
||||
err = json.Unmarshal(ms.Type, &s.Types)
|
||||
default:
|
||||
err = fmt.Errorf(`invalid value for "type": %q`, ms.Type)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal "items" as either Items or ItemsArray.
|
||||
if len(ms.Items) > 0 {
|
||||
switch ms.Items[0] {
|
||||
case '[':
|
||||
var schemas []*Schema
|
||||
err = json.Unmarshal(ms.Items, &schemas)
|
||||
s.ItemsArray = schemas
|
||||
default:
|
||||
var schema Schema
|
||||
err = json.Unmarshal(ms.Items, &schema)
|
||||
s.Items = &schema
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal "Dependencies" values as either string arrays or schemas
|
||||
// and assign them to specific map DependencySchemas or DependencyStrings.
|
||||
for k, v := range ms.Dependencies {
|
||||
if len(v) > 0 {
|
||||
switch v[0] {
|
||||
case '[':
|
||||
var dstrings []string
|
||||
err = json.Unmarshal(v, &dstrings)
|
||||
if s.DependencyStrings == nil {
|
||||
s.DependencyStrings = make(map[string][]string)
|
||||
}
|
||||
s.DependencyStrings[k] = dstrings
|
||||
default:
|
||||
var dschema Schema
|
||||
err = json.Unmarshal(v, &dschema)
|
||||
if s.DependencySchemas == nil {
|
||||
s.DependencySchemas = make(map[string]*Schema)
|
||||
}
|
||||
s.DependencySchemas[k] = &dschema
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
unmarshalAnyPtr := func(p **any, raw json.RawMessage) error {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(raw, []byte("null")) {
|
||||
*p = new(any)
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(raw, p)
|
||||
}
|
||||
|
||||
// Setting Const to a pointer to null will marshal properly, but won't
|
||||
// unmarshal: the *any is set to nil, not a pointer to nil.
|
||||
if err := unmarshalAnyPtr(&s.Const, ms.Const); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set := func(dst **int, src *integer) {
|
||||
if src != nil {
|
||||
*dst = Ptr(int(*src))
|
||||
}
|
||||
}
|
||||
|
||||
set(&s.MinLength, ms.MinLength)
|
||||
set(&s.MaxLength, ms.MaxLength)
|
||||
set(&s.MinItems, ms.MinItems)
|
||||
set(&s.MaxItems, ms.MaxItems)
|
||||
set(&s.MinProperties, ms.MinProperties)
|
||||
set(&s.MaxProperties, ms.MaxProperties)
|
||||
set(&s.MinContains, ms.MinContains)
|
||||
set(&s.MaxContains, ms.MaxContains)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type integer int32 // for the integer-valued fields of Schema
|
||||
|
||||
func (ip *integer) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// nothing to do
|
||||
return nil
|
||||
}
|
||||
// If there is a decimal point, src is a floating-point number.
|
||||
var i int64
|
||||
if bytes.ContainsRune(data, '.') {
|
||||
var f float64
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
return errors.New("not a number")
|
||||
}
|
||||
i = int64(f)
|
||||
if float64(i) != f {
|
||||
return errors.New("not an integer value")
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal(data, &i); err != nil {
|
||||
return errors.New("cannot be unmarshaled into an int")
|
||||
}
|
||||
}
|
||||
// Ensure behavior is the same on both 32-bit and 64-bit systems.
|
||||
if i < math.MinInt32 || i > math.MaxInt32 {
|
||||
return errors.New("integer is out of range")
|
||||
}
|
||||
*ip = integer(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to a new variable whose value is x.
|
||||
func Ptr[T any](x T) *T { return &x }
|
||||
|
||||
// every applies f preorder to every schema under s including s.
|
||||
// The second argument to f is the path to the schema appended to the argument path.
|
||||
// It stops when f returns false.
|
||||
func (s *Schema) every(f func(*Schema) bool) bool {
|
||||
return f(s) && s.everyChild(func(s *Schema) bool { return s.every(f) })
|
||||
}
|
||||
|
||||
// everyChild reports whether f is true for every immediate child schema of s.
|
||||
func (s *Schema) everyChild(f func(*Schema) bool) bool {
|
||||
v := reflect.ValueOf(s)
|
||||
for _, info := range schemaFieldInfos {
|
||||
fv := v.Elem().FieldByIndex(info.sf.Index)
|
||||
switch info.sf.Type {
|
||||
case schemaType:
|
||||
// A field that contains an individual schema. A nil is valid: it just means the field isn't present.
|
||||
c := fv.Interface().(*Schema)
|
||||
if c != nil && !f(c) {
|
||||
return false
|
||||
}
|
||||
|
||||
case schemaSliceType:
|
||||
slice := fv.Interface().([]*Schema)
|
||||
for _, c := range slice {
|
||||
if !f(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case schemaMapType:
|
||||
// Sort keys for determinism.
|
||||
m := fv.Interface().(map[string]*Schema)
|
||||
for _, k := range slices.Sorted(maps.Keys(m)) {
|
||||
if !f(m[k]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// all wraps every in an iterator.
|
||||
func (s *Schema) all() iter.Seq[*Schema] {
|
||||
return func(yield func(*Schema) bool) { s.every(yield) }
|
||||
}
|
||||
|
||||
// children wraps everyChild in an iterator.
|
||||
func (s *Schema) children() iter.Seq[*Schema] {
|
||||
return func(yield func(*Schema) bool) { s.everyChild(yield) }
|
||||
}
|
||||
|
||||
var (
|
||||
schemaType = reflect.TypeFor[*Schema]()
|
||||
schemaSliceType = reflect.TypeFor[[]*Schema]()
|
||||
schemaMapType = reflect.TypeFor[map[string]*Schema]()
|
||||
)
|
||||
|
||||
type structFieldInfo struct {
|
||||
sf reflect.StructField
|
||||
jsonName string
|
||||
}
|
||||
|
||||
var (
|
||||
// the visible fields of Schema that have a JSON name, sorted by that name
|
||||
schemaFieldInfos []structFieldInfo
|
||||
// map from JSON name to field
|
||||
schemaFieldMap = map[string]reflect.StructField{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
t := reflect.VisibleFields(reflect.TypeFor[Schema]())
|
||||
for _, sf := range t {
|
||||
info := fieldJSONInfo(sf)
|
||||
if !info.omit {
|
||||
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, info.name})
|
||||
} else {
|
||||
// jsoninfo.name is used to build the info paths. The items and dependencies are ommited,
|
||||
// since the original fields are separated to handle the union types supported in json and
|
||||
// these fields have custom marshalling and unmarshalling logic.
|
||||
// we still need these fields in schemaFieldInfos for creating schema trees and calculating paths and refs.
|
||||
// so we manually create them and assign the jsonName to the original field json name.
|
||||
switch sf.Name {
|
||||
case "Items", "ItemsArray":
|
||||
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "items"})
|
||||
case "DependencySchemas", "DependencyStrings":
|
||||
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "dependencies"})
|
||||
}
|
||||
}
|
||||
}
|
||||
// The value of "dependencies" this sort of schemaFieldInfos.
|
||||
// This sort is unstable and is comparing the json.names of DependencyStrings and DependencySchemas which are both "dependencies".
|
||||
// Since the sort is unstable it cannot be guarantied that "dependencies" has the DependencySchemas value.
|
||||
slices.SortFunc(schemaFieldInfos, func(i1, i2 structFieldInfo) int {
|
||||
return cmp.Compare(i1.jsonName, i2.jsonName)
|
||||
})
|
||||
for _, info := range schemaFieldInfos {
|
||||
schemaFieldMap[info.jsonName] = info.sf
|
||||
}
|
||||
}
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Equal reports whether two Go values representing JSON values are equal according
|
||||
// to the JSON Schema spec.
|
||||
// The values must not contain cycles.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-4.2.2.
|
||||
// It behaves like reflect.DeepEqual, except that numbers are compared according
|
||||
// to mathematical equality.
|
||||
func Equal(x, y any) bool {
|
||||
return equalValue(reflect.ValueOf(x), reflect.ValueOf(y))
|
||||
}
|
||||
|
||||
func equalValue(x, y reflect.Value) bool {
|
||||
// Copied from src/reflect/deepequal.go, omitting the visited check (because JSON
|
||||
// values are trees).
|
||||
if !x.IsValid() || !y.IsValid() {
|
||||
return x.IsValid() == y.IsValid()
|
||||
}
|
||||
|
||||
// Treat numbers specially.
|
||||
rx, ok1 := jsonNumber(x)
|
||||
ry, ok2 := jsonNumber(y)
|
||||
if ok1 && ok2 {
|
||||
return rx.Cmp(ry) == 0
|
||||
}
|
||||
if x.Kind() != y.Kind() {
|
||||
return false
|
||||
}
|
||||
switch x.Kind() {
|
||||
case reflect.Array:
|
||||
if x.Len() != y.Len() {
|
||||
return false
|
||||
}
|
||||
for i := range x.Len() {
|
||||
if !equalValue(x.Index(i), y.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Slice:
|
||||
if x.IsNil() != y.IsNil() {
|
||||
return false
|
||||
}
|
||||
if x.Len() != y.Len() {
|
||||
return false
|
||||
}
|
||||
if x.UnsafePointer() == y.UnsafePointer() {
|
||||
return true
|
||||
}
|
||||
// Special case for []byte, which is common.
|
||||
if x.Type().Elem().Kind() == reflect.Uint8 && x.Type() == y.Type() {
|
||||
return bytes.Equal(x.Bytes(), y.Bytes())
|
||||
}
|
||||
for i := range x.Len() {
|
||||
if !equalValue(x.Index(i), y.Index(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Interface:
|
||||
if x.IsNil() || y.IsNil() {
|
||||
return x.IsNil() == y.IsNil()
|
||||
}
|
||||
return equalValue(x.Elem(), y.Elem())
|
||||
case reflect.Pointer:
|
||||
if x.UnsafePointer() == y.UnsafePointer() {
|
||||
return true
|
||||
}
|
||||
return equalValue(x.Elem(), y.Elem())
|
||||
case reflect.Struct:
|
||||
t := x.Type()
|
||||
if t != y.Type() {
|
||||
return false
|
||||
}
|
||||
for i := range t.NumField() {
|
||||
sf := t.Field(i)
|
||||
if !sf.IsExported() {
|
||||
continue
|
||||
}
|
||||
if !equalValue(x.FieldByIndex(sf.Index), y.FieldByIndex(sf.Index)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Map:
|
||||
if x.IsNil() != y.IsNil() {
|
||||
return false
|
||||
}
|
||||
if x.Len() != y.Len() {
|
||||
return false
|
||||
}
|
||||
if x.UnsafePointer() == y.UnsafePointer() {
|
||||
return true
|
||||
}
|
||||
iter := x.MapRange()
|
||||
for iter.Next() {
|
||||
vx := iter.Value()
|
||||
vy := y.MapIndex(iter.Key())
|
||||
if !vy.IsValid() || !equalValue(vx, vy) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Func:
|
||||
if x.Type() != y.Type() {
|
||||
return false
|
||||
}
|
||||
if x.IsNil() && y.IsNil() {
|
||||
return true
|
||||
}
|
||||
panic("cannot compare functions")
|
||||
case reflect.String:
|
||||
return x.String() == y.String()
|
||||
case reflect.Bool:
|
||||
return x.Bool() == y.Bool()
|
||||
// Ints, uints and floats handled in jsonNumber, at top of function.
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported kind: %s", x.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// hashValue adds v to the data hashed by h. v must not have cycles.
|
||||
// hashValue panics if the value contains functions or channels, or maps whose
|
||||
// key type is not string.
|
||||
// It ignores unexported fields of structs.
|
||||
// Calls to hashValue with the equal values (in the sense
|
||||
// of [Equal]) result in the same sequence of values written to the hash.
|
||||
func hashValue(h *maphash.Hash, v reflect.Value) {
|
||||
// TODO: replace writes of basic types with WriteComparable in 1.24.
|
||||
|
||||
writeUint := func(u uint64) {
|
||||
var buf [8]byte
|
||||
binary.BigEndian.PutUint64(buf[:], u)
|
||||
h.Write(buf[:])
|
||||
}
|
||||
|
||||
var write func(reflect.Value)
|
||||
write = func(v reflect.Value) {
|
||||
if r, ok := jsonNumber(v); ok {
|
||||
// We want 1.0 and 1 to hash the same.
|
||||
// big.Rats are always normalized, so they will be.
|
||||
// We could do this more efficiently by handling the int and float cases
|
||||
// separately, but that's premature.
|
||||
writeUint(uint64(r.Sign() + 1))
|
||||
h.Write(r.Num().Bytes())
|
||||
h.Write(r.Denom().Bytes())
|
||||
return
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Invalid:
|
||||
h.WriteByte(0)
|
||||
case reflect.String:
|
||||
h.WriteString(v.String())
|
||||
case reflect.Bool:
|
||||
if v.Bool() {
|
||||
h.WriteByte(1)
|
||||
} else {
|
||||
h.WriteByte(0)
|
||||
}
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
c := v.Complex()
|
||||
writeUint(math.Float64bits(real(c)))
|
||||
writeUint(math.Float64bits(imag(c)))
|
||||
case reflect.Array, reflect.Slice:
|
||||
// Although we could treat []byte more efficiently,
|
||||
// JSON values are unlikely to contain them.
|
||||
writeUint(uint64(v.Len()))
|
||||
for i := range v.Len() {
|
||||
write(v.Index(i))
|
||||
}
|
||||
case reflect.Interface, reflect.Pointer:
|
||||
write(v.Elem())
|
||||
case reflect.Struct:
|
||||
t := v.Type()
|
||||
for i := range t.NumField() {
|
||||
if sf := t.Field(i); sf.IsExported() {
|
||||
write(v.FieldByIndex(sf.Index))
|
||||
}
|
||||
}
|
||||
case reflect.Map:
|
||||
if v.Type().Key().Kind() != reflect.String {
|
||||
panic("map with non-string key")
|
||||
}
|
||||
// Sort the keys so the hash is deterministic.
|
||||
keys := v.MapKeys()
|
||||
// Write the length. That distinguishes between, say, two consecutive
|
||||
// maps with disjoint keys from one map that has the items of both.
|
||||
writeUint(uint64(len(keys)))
|
||||
slices.SortFunc(keys, func(x, y reflect.Value) int { return cmp.Compare(x.String(), y.String()) })
|
||||
for _, k := range keys {
|
||||
write(k)
|
||||
write(v.MapIndex(k))
|
||||
}
|
||||
// Ints, uints and floats handled in jsonNumber, at top of function.
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported kind: %s", v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
write(v)
|
||||
}
|
||||
|
||||
// jsonNumber converts a numeric value or a json.Number to a [big.Rat].
|
||||
// If v is not a number, it returns nil, false.
|
||||
func jsonNumber(v reflect.Value) (*big.Rat, bool) {
|
||||
r := new(big.Rat)
|
||||
switch {
|
||||
case !v.IsValid():
|
||||
return nil, false
|
||||
case v.CanInt():
|
||||
r.SetInt64(v.Int())
|
||||
case v.CanUint():
|
||||
r.SetUint64(v.Uint())
|
||||
case v.CanFloat():
|
||||
r.SetFloat64(v.Float())
|
||||
default:
|
||||
jn, ok := v.Interface().(json.Number)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if _, ok := r.SetString(jn.String()); !ok {
|
||||
// This can fail in rare cases; for example, "1e9999999".
|
||||
// That is a valid JSON number, since the spec puts no limit on the size
|
||||
// of the exponent.
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
|
||||
// jsonType returns a string describing the type of the JSON value,
|
||||
// as described in the JSON Schema specification:
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1.
|
||||
// It returns "", false if the value is not valid JSON.
|
||||
func jsonType(v reflect.Value) (string, bool) {
|
||||
if !v.IsValid() {
|
||||
// Not v.IsNil(): a nil []any is still a JSON array.
|
||||
return "null", true
|
||||
}
|
||||
if v.CanInt() || v.CanUint() {
|
||||
return "integer", true
|
||||
}
|
||||
if v.CanFloat() {
|
||||
if _, f := math.Modf(v.Float()); f == 0 {
|
||||
return "integer", true
|
||||
}
|
||||
return "number", true
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return "boolean", true
|
||||
case reflect.String:
|
||||
return "string", true
|
||||
case reflect.Slice, reflect.Array:
|
||||
return "array", true
|
||||
case reflect.Map, reflect.Struct:
|
||||
return "object", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func assert(cond bool, msg string) {
|
||||
if !cond {
|
||||
panic("assertion failed: " + msg)
|
||||
}
|
||||
}
|
||||
|
||||
// marshalStructWithMap marshals its first argument to JSON, treating the field named
|
||||
// mapField as an embedded map. The first argument must be a pointer to
|
||||
// a struct. The underlying type of mapField must be a map[string]any, and it must have
|
||||
// a "-" json tag, meaning it will not be marshaled.
|
||||
//
|
||||
// For example, given this struct:
|
||||
//
|
||||
// type S struct {
|
||||
// A int
|
||||
// Extra map[string] any `json:"-"`
|
||||
// }
|
||||
//
|
||||
// and this value:
|
||||
//
|
||||
// s := S{A: 1, Extra: map[string]any{"B": 2}}
|
||||
//
|
||||
// the call marshalJSONWithMap(s, "Extra") would return
|
||||
//
|
||||
// {"A": 1, "B": 2}
|
||||
//
|
||||
// It is an error if the map contains the same key as another struct field's
|
||||
// JSON name.
|
||||
//
|
||||
// marshalStructWithMap calls json.Marshal on a value of type T, so T must not
|
||||
// have a MarshalJSON method that calls this function, on pain of infinite regress.
|
||||
//
|
||||
// Note that there is a similar function in mcp/util.go, but they are not the same.
|
||||
// Here the function requires `-` json tag, does not clear the mapField map,
|
||||
// and handles embedded struct due to the implementation of jsonNames in this package.
|
||||
//
|
||||
// TODO: avoid this restriction on T by forcing it to marshal in a default way.
|
||||
// See https://go.dev/play/p/EgXKJHxEx_R.
|
||||
func marshalStructWithMap[T any](s *T, mapField string) ([]byte, error) {
|
||||
// Marshal the struct and the map separately, and concatenate the bytes.
|
||||
// This strategy is dramatically less complicated than
|
||||
// constructing a synthetic struct or map with the combined keys.
|
||||
if s == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
s2 := *s
|
||||
vMapField := reflect.ValueOf(&s2).Elem().FieldByName(mapField)
|
||||
mapVal := vMapField.Interface().(map[string]any)
|
||||
|
||||
// Check for duplicates.
|
||||
names := jsonNames(reflect.TypeFor[T]())
|
||||
for key := range mapVal {
|
||||
if names[key] {
|
||||
return nil, fmt.Errorf("map key %q duplicates struct field", key)
|
||||
}
|
||||
}
|
||||
|
||||
structBytes, err := json.Marshal(s2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshalStructWithMap(%+v): %w", s, err)
|
||||
}
|
||||
if len(mapVal) == 0 {
|
||||
return structBytes, nil
|
||||
}
|
||||
mapBytes, err := json.Marshal(mapVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(structBytes) == 2 { // must be "{}"
|
||||
return mapBytes, nil
|
||||
}
|
||||
// "{X}" + "{Y}" => "{X,Y}"
|
||||
res := append(structBytes[:len(structBytes)-1], ',')
|
||||
res = append(res, mapBytes[1:]...)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// unmarshalStructWithMap is the inverse of marshalStructWithMap.
|
||||
// T has the same restrictions as in that function.
|
||||
//
|
||||
// Note that there is a similar function in mcp/util.go, but they are not the same.
|
||||
// Here jsonNames also returns fields from embedded structs, hence this function
|
||||
// handles embedded structs as well.
|
||||
func unmarshalStructWithMap[T any](data []byte, v *T, mapField string) error {
|
||||
// Unmarshal into the struct, ignoring unknown fields.
|
||||
if err := json.Unmarshal(data, v); err != nil {
|
||||
return err
|
||||
}
|
||||
// Unmarshal into the map.
|
||||
m := map[string]any{}
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
// Delete from the map the fields of the struct.
|
||||
for n := range jsonNames(reflect.TypeFor[T]()) {
|
||||
delete(m, n)
|
||||
}
|
||||
if len(m) != 0 {
|
||||
reflect.ValueOf(v).Elem().FieldByName(mapField).Set(reflect.ValueOf(m))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var jsonNamesMap sync.Map // from reflect.Type to map[string]bool
|
||||
|
||||
// jsonNames returns the set of JSON object keys that t will marshal into,
|
||||
// including fields from embedded structs in t.
|
||||
// t must be a struct type.
|
||||
//
|
||||
// Note that there is a similar function in mcp/util.go, but they are not the same
|
||||
// Here the function recurses over embedded structs and includes fields from them.
|
||||
func jsonNames(t reflect.Type) map[string]bool {
|
||||
// Lock not necessary: at worst we'll duplicate work.
|
||||
if val, ok := jsonNamesMap.Load(t); ok {
|
||||
return val.(map[string]bool)
|
||||
}
|
||||
m := map[string]bool{}
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
// handle embedded structs
|
||||
if field.Anonymous {
|
||||
fieldType := field.Type
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
for n := range jsonNames(fieldType) {
|
||||
m[n] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
info := fieldJSONInfo(field)
|
||||
if !info.omit {
|
||||
m[info.name] = true
|
||||
}
|
||||
}
|
||||
jsonNamesMap.Store(t, m)
|
||||
return m
|
||||
}
|
||||
|
||||
type jsonInfo struct {
|
||||
omit bool // unexported or first tag element is "-"
|
||||
name string // Go field name or first tag element. Empty if omit is true.
|
||||
settings map[string]bool // "omitempty", "omitzero", etc.
|
||||
}
|
||||
|
||||
// fieldJSONInfo reports information about how encoding/json
|
||||
// handles the given struct field.
|
||||
// If the field is unexported, jsonInfo.omit is true and no other jsonInfo field
|
||||
// is populated.
|
||||
// If the field is exported and has no tag, then name is the field's name and all
|
||||
// other fields are false.
|
||||
// Otherwise, the information is obtained from the tag.
|
||||
func fieldJSONInfo(f reflect.StructField) jsonInfo {
|
||||
if !f.IsExported() {
|
||||
return jsonInfo{omit: true}
|
||||
}
|
||||
info := jsonInfo{name: f.Name}
|
||||
if tag, ok := f.Tag.Lookup("json"); ok {
|
||||
name, rest, found := strings.Cut(tag, ",")
|
||||
// "-" means omit, but "-," means the name is "-"
|
||||
if name == "-" && !found {
|
||||
return jsonInfo{omit: true}
|
||||
}
|
||||
if name != "" {
|
||||
info.name = name
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
info.settings = map[string]bool{}
|
||||
for _, s := range strings.Split(rest, ",") {
|
||||
info.settings[s] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// wrapf wraps *errp with the given formatted message if *errp is not nil.
|
||||
func wrapf(errp *error, format string, args ...any) {
|
||||
if *errp != nil {
|
||||
*errp = fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), *errp)
|
||||
}
|
||||
}
|
||||
+906
@@ -0,0 +1,906 @@
|
||||
// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"iter"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// The values of the "$schema" keyword for the versions that we can validate.
|
||||
const (
|
||||
draft7SchemaVersion = "http://json-schema.org/draft-07/schema#"
|
||||
draft7SecSchemaVersion = "https://json-schema.org/draft-07/schema#"
|
||||
draft202012SchemaVersion = "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 == draft7SchemaVersion || version == draft7SecSchemaVersion || version == draft202012SchemaVersion
|
||||
}
|
||||
|
||||
// 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 should be a map[string]any.
|
||||
func (rs *Resolved) Validate(instance any) error {
|
||||
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)
|
||||
}
|
||||
|
||||
// validateDefaults walks the schema tree. If it finds a default, it validates it
|
||||
// against the schema containing it.
|
||||
//
|
||||
// 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; !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() {
|
||||
// We checked for nil schemas in [Schema.Resolve].
|
||||
assert(s != nil, "nil schema")
|
||||
if s.DynamicRef != "" {
|
||||
return fmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", rs.schemaString(s))
|
||||
}
|
||||
if s.Default != nil {
|
||||
var d any
|
||||
if err := json.Unmarshal(s.Default, &d); err != nil {
|
||||
return fmt.Errorf("unmarshaling default value of schema %s: %w", rs.schemaString(s), err)
|
||||
}
|
||||
if err := st.validate(reflect.ValueOf(d), s, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// state is the state of single call to ResolvedSchema.Validate.
|
||||
type state struct {
|
||||
rs *Resolved
|
||||
// stack holds the schemas from recursive calls to validate.
|
||||
// These are the "dynamic scopes" used to resolve dynamic references.
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#scopes
|
||||
stack []*Schema
|
||||
}
|
||||
|
||||
// validate validates the reflected value of the instance.
|
||||
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) {
|
||||
defer wrapf(&err, "validating %s", st.rs.schemaString(schema))
|
||||
|
||||
// Maintain a stack for dynamic schema resolution.
|
||||
st.stack = append(st.stack, schema) // push
|
||||
defer func() {
|
||||
st.stack = st.stack[:len(st.stack)-1] // pop
|
||||
}()
|
||||
|
||||
// We checked for nil schemas in [Schema.Resolve].
|
||||
assert(schema != nil, "nil schema")
|
||||
|
||||
// Step through interfaces and pointers.
|
||||
for instance.Kind() == reflect.Pointer || instance.Kind() == reflect.Interface {
|
||||
instance = instance.Elem()
|
||||
}
|
||||
|
||||
schemaInfo := st.rs.resolvedInfos[schema]
|
||||
|
||||
var anns annotations // all the annotations for this call and child calls
|
||||
// $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1
|
||||
if schema.Ref != "" {
|
||||
if err := st.validate(instance, schemaInfo.resolvedRef, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3
|
||||
// "All other properties in a "$ref" object MUST be ignored."
|
||||
if st.rs.draft == draft7 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// type: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1
|
||||
if schema.Type != "" || schema.Types != nil {
|
||||
gotType, ok := jsonType(instance)
|
||||
if !ok {
|
||||
return fmt.Errorf("type: %v of type %[1]T is not a valid JSON value", instance)
|
||||
}
|
||||
if schema.Type != "" {
|
||||
// "number" subsumes integers
|
||||
if !(gotType == schema.Type ||
|
||||
gotType == "integer" && schema.Type == "number") {
|
||||
return fmt.Errorf("type: %v has type %q, want %q", instance, gotType, schema.Type)
|
||||
}
|
||||
} else {
|
||||
if !(slices.Contains(schema.Types, gotType) || (gotType == "integer" && slices.Contains(schema.Types, "number"))) {
|
||||
return fmt.Errorf("type: %v has type %q, want one of %q",
|
||||
instance, gotType, strings.Join(schema.Types, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
// enum: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.2
|
||||
if schema.Enum != nil {
|
||||
ok := false
|
||||
for _, e := range schema.Enum {
|
||||
if equalValue(reflect.ValueOf(e), instance) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("enum: %v does not equal any of: %v", instance, schema.Enum)
|
||||
}
|
||||
}
|
||||
|
||||
// const: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.3
|
||||
if schema.Const != nil {
|
||||
if !equalValue(reflect.ValueOf(*schema.Const), instance) {
|
||||
return fmt.Errorf("const: %v does not equal %v", instance, *schema.Const)
|
||||
}
|
||||
}
|
||||
|
||||
// numbers: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.2
|
||||
if schema.MultipleOf != nil || schema.Minimum != nil || schema.Maximum != nil || schema.ExclusiveMinimum != nil || schema.ExclusiveMaximum != nil {
|
||||
n, ok := jsonNumber(instance)
|
||||
if ok { // these keywords don't apply to non-numbers
|
||||
if schema.MultipleOf != nil {
|
||||
// TODO: validate MultipleOf as non-zero.
|
||||
// The test suite assumes floats.
|
||||
nf, _ := n.Float64() // don't care if it's exact or not
|
||||
if _, f := math.Modf(nf / *schema.MultipleOf); f != 0 {
|
||||
return fmt.Errorf("multipleOf: %s is not a multiple of %f", n, *schema.MultipleOf)
|
||||
}
|
||||
}
|
||||
|
||||
m := new(big.Rat) // reuse for all of the following
|
||||
cmp := func(f float64) int { return n.Cmp(m.SetFloat64(f)) }
|
||||
|
||||
if schema.Minimum != nil && cmp(*schema.Minimum) < 0 {
|
||||
return fmt.Errorf("minimum: %s is less than %f", n, *schema.Minimum)
|
||||
}
|
||||
if schema.Maximum != nil && cmp(*schema.Maximum) > 0 {
|
||||
return fmt.Errorf("maximum: %s is greater than %f", n, *schema.Maximum)
|
||||
}
|
||||
if schema.ExclusiveMinimum != nil && cmp(*schema.ExclusiveMinimum) <= 0 {
|
||||
return fmt.Errorf("exclusiveMinimum: %s is less than or equal to %f", n, *schema.ExclusiveMinimum)
|
||||
}
|
||||
if schema.ExclusiveMaximum != nil && cmp(*schema.ExclusiveMaximum) >= 0 {
|
||||
return fmt.Errorf("exclusiveMaximum: %s is greater than or equal to %f", n, *schema.ExclusiveMaximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// strings: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.3
|
||||
if instance.Kind() == reflect.String && (schema.MinLength != nil || schema.MaxLength != nil || schema.Pattern != "") {
|
||||
str := instance.String()
|
||||
n := utf8.RuneCountInString(str)
|
||||
if schema.MinLength != nil {
|
||||
if m := *schema.MinLength; n < m {
|
||||
return fmt.Errorf("minLength: %q contains %d Unicode code points, fewer than %d", str, n, m)
|
||||
}
|
||||
}
|
||||
if schema.MaxLength != nil {
|
||||
if m := *schema.MaxLength; n > m {
|
||||
return fmt.Errorf("maxLength: %q contains %d Unicode code points, more than %d", str, n, m)
|
||||
}
|
||||
}
|
||||
|
||||
if schema.Pattern != "" && !schemaInfo.pattern.MatchString(str) {
|
||||
return fmt.Errorf("pattern: %q does not match regular expression %q", str, schema.Pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// $dynamicRef: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2
|
||||
if schema.DynamicRef != "" {
|
||||
// The ref behaves lexically or dynamically, but not both.
|
||||
assert((schemaInfo.resolvedDynamicRef == nil) != (schemaInfo.dynamicRefAnchor == ""),
|
||||
"DynamicRef not resolved properly")
|
||||
if schemaInfo.resolvedDynamicRef != nil {
|
||||
// Same as $ref.
|
||||
if err := st.validate(instance, schemaInfo.resolvedDynamicRef, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Dynamic behavior.
|
||||
// Look for the base of the outermost schema on the stack with this dynamic
|
||||
// anchor. (Yes, outermost: the one farthest from here. This the opposite
|
||||
// of how ordinary dynamic variables behave.)
|
||||
// Why the base of the schema being validated and not the schema itself?
|
||||
// Because the base is the scope for anchors. In fact it's possible to
|
||||
// refer to a schema that is not on the stack, but a child of some base
|
||||
// on the stack.
|
||||
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
||||
var dynamicSchema *Schema
|
||||
for _, s := range st.stack {
|
||||
base := st.rs.resolvedInfos[s].base
|
||||
info, ok := st.rs.resolvedInfos[base].anchors[schemaInfo.dynamicRefAnchor]
|
||||
if ok && info.dynamic {
|
||||
dynamicSchema = info.schema
|
||||
break
|
||||
}
|
||||
}
|
||||
if dynamicSchema == nil {
|
||||
return fmt.Errorf("missing dynamic anchor %q", schemaInfo.dynamicRefAnchor)
|
||||
}
|
||||
if err := st.validate(instance, dynamicSchema, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logic
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2
|
||||
// These must happen before arrays and objects because if they evaluate an item or property,
|
||||
// then the unevaluatedItems/Properties schemas don't apply to it.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-11.2, paragraph 4.
|
||||
//
|
||||
// If any of these fail, then validation fails, even if there is an unevaluatedXXX
|
||||
// keyword in the schema. The spec is unclear about this, but that is the intention.
|
||||
|
||||
valid := func(s *Schema, anns *annotations) bool { return st.validate(instance, s, anns) == nil }
|
||||
|
||||
if schema.AllOf != nil {
|
||||
for _, ss := range schema.AllOf {
|
||||
if err := st.validate(instance, ss, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.AnyOf != nil {
|
||||
// We must visit them all, to collect annotations.
|
||||
var errs []error
|
||||
for _, ss := range schema.AnyOf {
|
||||
if err := st.validate(instance, ss, &anns); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) == len(schema.AnyOf) {
|
||||
return fmt.Errorf("anyOf: did not validate against any of %v:\n%v",
|
||||
schema.AnyOf, errors.Join(errs...))
|
||||
}
|
||||
}
|
||||
if schema.OneOf != nil {
|
||||
// Exactly one.
|
||||
var okSchema *Schema
|
||||
for _, ss := range schema.OneOf {
|
||||
if valid(ss, &anns) {
|
||||
if okSchema != nil {
|
||||
return fmt.Errorf("oneOf: validated against both %v and %v", okSchema, ss)
|
||||
}
|
||||
okSchema = ss
|
||||
}
|
||||
}
|
||||
if okSchema == nil {
|
||||
return fmt.Errorf("oneOf: did not validate against any of %v", schema.OneOf)
|
||||
}
|
||||
}
|
||||
if schema.Not != nil {
|
||||
// Ignore annotations from "not".
|
||||
if valid(schema.Not, nil) {
|
||||
return fmt.Errorf("not: validated against %v", schema.Not)
|
||||
}
|
||||
}
|
||||
if schema.If != nil {
|
||||
var ss *Schema
|
||||
if valid(schema.If, &anns) {
|
||||
ss = schema.Then
|
||||
} else {
|
||||
ss = schema.Else
|
||||
}
|
||||
if ss != nil {
|
||||
if err := st.validate(instance, ss, &anns); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// arrays
|
||||
if instance.Kind() == reflect.Array || instance.Kind() == reflect.Slice {
|
||||
// Handle both draft-07 and draft 2020-12
|
||||
// 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.
|
||||
// TODO(jba): if the test suite doesn't cover this case, add a test. For example, nested arrays.
|
||||
if st.rs.draft == draft7 {
|
||||
// For draft-07: additionalItems applies to remaining items after items array.
|
||||
// If items is a Schema or if items is not set, additionalItems should be ignored
|
||||
if schema.ItemsArray != nil {
|
||||
for i, ischema := range schema.ItemsArray {
|
||||
if i >= instance.Len() {
|
||||
break // shorter is OK
|
||||
}
|
||||
if err := st.validate(instance.Index(i), ischema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
anns.noteEndIndex(min(len(schema.ItemsArray), instance.Len()))
|
||||
if schema.AdditionalItems != nil {
|
||||
for i := len(schema.ItemsArray); i < instance.Len(); i++ {
|
||||
if err := st.validate(instance.Index(i), schema.AdditionalItems, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
anns.allItems = true
|
||||
}
|
||||
} else if schema.Items != nil {
|
||||
for i := 0; i < instance.Len(); i++ {
|
||||
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Note that all the items in this array have been validated.
|
||||
anns.allItems = true
|
||||
}
|
||||
} else if st.rs.draft == draft2020 {
|
||||
// For draft 2020-12: items applies to remaining items after prefixItems
|
||||
for i, ischema := range schema.PrefixItems {
|
||||
if i >= instance.Len() {
|
||||
break // shorter is OK
|
||||
}
|
||||
if err := st.validate(instance.Index(i), ischema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
anns.noteEndIndex(min(len(schema.PrefixItems), instance.Len()))
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Note that all the items in this array have been validated.
|
||||
anns.allItems = true
|
||||
}
|
||||
}
|
||||
nContains := 0
|
||||
if schema.Contains != nil {
|
||||
for i := range instance.Len() {
|
||||
if err := st.validate(instance.Index(i), schema.Contains, nil); err == nil {
|
||||
nContains++
|
||||
anns.noteIndex(i)
|
||||
}
|
||||
}
|
||||
if nContains == 0 && (schema.MinContains == nil || *schema.MinContains > 0) {
|
||||
return fmt.Errorf("contains: %s does not have an item matching %s", instance, schema.Contains)
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.4
|
||||
// TODO(jba): check that these next four keywords' values are integers.
|
||||
if schema.MinContains != nil && schema.Contains != nil {
|
||||
if m := *schema.MinContains; nContains < m {
|
||||
return fmt.Errorf("minContains: contains validated %d items, less than %d", nContains, m)
|
||||
}
|
||||
}
|
||||
if schema.MaxContains != nil && schema.Contains != nil {
|
||||
if m := *schema.MaxContains; nContains > m {
|
||||
return fmt.Errorf("maxContains: contains validated %d items, greater than %d", nContains, m)
|
||||
}
|
||||
}
|
||||
if schema.MinItems != nil {
|
||||
if m := *schema.MinItems; instance.Len() < m {
|
||||
return fmt.Errorf("minItems: array length %d is less than %d", instance.Len(), m)
|
||||
}
|
||||
}
|
||||
if schema.MaxItems != nil {
|
||||
if m := *schema.MaxItems; instance.Len() > m {
|
||||
return fmt.Errorf("maxItems: array length %d is greater than %d", instance.Len(), m)
|
||||
}
|
||||
}
|
||||
if schema.UniqueItems {
|
||||
if instance.Len() > 1 {
|
||||
// Hash each item and compare the hashes.
|
||||
// If two hashes differ, the items differ.
|
||||
// If two hashes are the same, compare the collisions for equality.
|
||||
// (The same logic as hash table lookup.)
|
||||
// TODO(jba): Use container/hash.Map when it becomes available (https://go.dev/issue/69559),
|
||||
hashes := map[uint64][]int{} // from hash to indices
|
||||
seed := maphash.MakeSeed()
|
||||
for i := range instance.Len() {
|
||||
item := instance.Index(i)
|
||||
var h maphash.Hash
|
||||
h.SetSeed(seed)
|
||||
hashValue(&h, item)
|
||||
hv := h.Sum64()
|
||||
if sames := hashes[hv]; len(sames) > 0 {
|
||||
for _, j := range sames {
|
||||
if equalValue(item, instance.Index(j)) {
|
||||
return fmt.Errorf("uniqueItems: array items %d and %d are equal", i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
hashes[hv] = append(hashes[hv], i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-11.2
|
||||
if schema.UnevaluatedItems != nil && !anns.allItems {
|
||||
// Apply this subschema to all items in the array that haven't been successfully validated.
|
||||
// That includes validations by subschemas on the same instance, like allOf.
|
||||
for i := anns.endIndex; i < instance.Len(); i++ {
|
||||
if !anns.evaluatedIndexes[i] {
|
||||
if err := st.validate(instance.Index(i), schema.UnevaluatedItems, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
anns.allItems = true
|
||||
}
|
||||
}
|
||||
|
||||
// objects
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.2
|
||||
// Validating structs is problematic. See https://github.com/google/jsonschema-go/issues/23.
|
||||
if instance.Kind() == reflect.Struct {
|
||||
return errors.New("cannot validate against a struct; see https://github.com/google/jsonschema-go/issues/23 for details")
|
||||
}
|
||||
if instance.Kind() == reflect.Map {
|
||||
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
|
||||
return fmt.Errorf("map key type %s is not a string", kt)
|
||||
}
|
||||
// Track the evaluated properties for just this schema, to support additionalProperties.
|
||||
// If we used anns here, then we'd be including properties evaluated in subschemas
|
||||
// from allOf, etc., which additionalProperties shouldn't observe.
|
||||
evalProps := map[string]bool{}
|
||||
for prop, subschema := range schema.Properties {
|
||||
val := property(instance, prop)
|
||||
if !val.IsValid() {
|
||||
// It's OK if the instance doesn't have the property.
|
||||
continue
|
||||
}
|
||||
// If the instance is a struct and an optional property has the zero
|
||||
// value, then we could interpret it as present or missing. Be generous:
|
||||
// assume it's missing, and thus always validates successfully.
|
||||
if instance.Kind() == reflect.Struct && val.IsZero() && !schemaInfo.isRequired[prop] {
|
||||
continue
|
||||
}
|
||||
if err := st.validate(val, subschema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
}
|
||||
if len(schema.PatternProperties) > 0 {
|
||||
for prop, val := range properties(instance) {
|
||||
// Check every matching pattern.
|
||||
for re, schema := range schemaInfo.patternProperties {
|
||||
if re.MatchString(prop) {
|
||||
if err := st.validate(val, schema, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.AdditionalProperties != nil {
|
||||
// Special case for a better error message when additional properties is
|
||||
// 'falsy'
|
||||
//
|
||||
// If additionalProperties is {"not":{}} (which is how we
|
||||
// unmarshal "false"), we can produce a better error message that
|
||||
// summarizes all the extra properties. Otherwise, we fall back to the
|
||||
// default validation.
|
||||
//
|
||||
// Note: this is much faster than comparing with falseSchema using Equal.
|
||||
isFalsy := schema.AdditionalProperties.Not != nil && reflect.ValueOf(*schema.AdditionalProperties.Not).IsZero()
|
||||
if isFalsy {
|
||||
var disallowed []string
|
||||
for prop := range properties(instance) {
|
||||
if !evalProps[prop] {
|
||||
disallowed = append(disallowed, prop)
|
||||
}
|
||||
}
|
||||
if len(disallowed) > 0 {
|
||||
return fmt.Errorf("unexpected additional properties %q", disallowed)
|
||||
}
|
||||
} else {
|
||||
// Apply to all properties not handled above.
|
||||
for prop, val := range properties(instance) {
|
||||
if !evalProps[prop] {
|
||||
if err := st.validate(val, schema.AdditionalProperties, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
evalProps[prop] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anns.noteProperties(evalProps)
|
||||
if schema.PropertyNames != nil {
|
||||
// Note: properties unnecessarily fetches each value. We could define a propertyNames function
|
||||
// if performance ever matters.
|
||||
for prop := range properties(instance) {
|
||||
if err := st.validate(reflect.ValueOf(prop), schema.PropertyNames, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5
|
||||
var min, max int
|
||||
if schema.MinProperties != nil || schema.MaxProperties != nil {
|
||||
min, max = numPropertiesBounds(instance, schemaInfo.isRequired)
|
||||
}
|
||||
if schema.MinProperties != nil {
|
||||
if n, m := max, *schema.MinProperties; n < m {
|
||||
return fmt.Errorf("minProperties: object has %d properties, less than %d", n, m)
|
||||
}
|
||||
}
|
||||
if schema.MaxProperties != nil {
|
||||
if n, m := min, *schema.MaxProperties; n > m {
|
||||
return fmt.Errorf("maxProperties: object has %d properties, greater than %d", n, m)
|
||||
}
|
||||
}
|
||||
|
||||
hasProperty := func(prop string) bool {
|
||||
return property(instance, prop).IsValid()
|
||||
}
|
||||
|
||||
missingProperties := func(props []string) []string {
|
||||
var missing []string
|
||||
for _, p := range props {
|
||||
if !hasProperty(p) {
|
||||
missing = append(missing, p)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
if schema.Required != nil {
|
||||
if m := missingProperties(schema.Required); len(m) > 0 {
|
||||
return fmt.Errorf("required: missing properties: %q", m)
|
||||
}
|
||||
}
|
||||
|
||||
if st.rs.draft == draft7 {
|
||||
if schema.DependencyStrings != nil {
|
||||
for dprop, dstrings := range schema.DependencyStrings {
|
||||
if hasProperty(dprop) {
|
||||
if m := missingProperties(dstrings); len(m) > 0 {
|
||||
return fmt.Errorf("dependentRequired[%q]: missing properties %q", dprop, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.DependencySchemas != nil {
|
||||
for dprop, dschema := range schema.DependencySchemas {
|
||||
if hasProperty(dprop) {
|
||||
err := st.validate(instance, dschema, &anns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if st.rs.draft == draft2020 {
|
||||
if schema.DependentRequired != nil {
|
||||
// "Validation succeeds if, for each name that appears in both the instance
|
||||
// and as a name within this keyword's value, every item in the corresponding
|
||||
// array is also the name of a property in the instance." §6.5.4
|
||||
for dprop, reqs := range schema.DependentRequired {
|
||||
if hasProperty(dprop) {
|
||||
if m := missingProperties(reqs); len(m) > 0 {
|
||||
return fmt.Errorf("dependentRequired[%q]: missing properties %q", dprop, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2.2.4
|
||||
if schema.DependentSchemas != nil {
|
||||
// This does not collect annotations, although it seems like it should.
|
||||
for dprop, ss := range schema.DependentSchemas {
|
||||
if hasProperty(dprop) {
|
||||
// TODO: include dependentSchemas[dprop] in the errors.
|
||||
err := st.validate(instance, ss, &anns)
|
||||
if err != nil {
|
||||
return 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.
|
||||
for prop, val := range properties(instance) {
|
||||
if !anns.evaluatedProperties[prop] {
|
||||
if err := st.validate(val, schema.UnevaluatedProperties, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// The spec says the annotation should be the set of evaluated properties, but we can optimize
|
||||
// by setting a single boolean, since after this succeeds all properties will be validated.
|
||||
// See https://json-schema.slack.com/archives/CT7FF623C/p1745592564381459.
|
||||
anns.allProperties = true
|
||||
}
|
||||
}
|
||||
|
||||
if callerAnns != nil {
|
||||
// Our caller wants to know what we've validated.
|
||||
callerAnns.merge(&anns)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveDynamicRef returns the schema referred to by the argument schema's
|
||||
// $dynamicRef value.
|
||||
// It returns an error if the dynamic reference has no referent.
|
||||
// If there is no $dynamicRef, resolveDynamicRef returns nil, nil.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2.
|
||||
func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
|
||||
if schema.DynamicRef == "" {
|
||||
return nil, nil
|
||||
}
|
||||
info := st.rs.resolvedInfos[schema]
|
||||
// The ref behaves lexically or dynamically, but not both.
|
||||
assert((info.resolvedDynamicRef == nil) != (info.dynamicRefAnchor == ""),
|
||||
"DynamicRef not statically resolved properly")
|
||||
if r := info.resolvedDynamicRef; r != nil {
|
||||
// Same as $ref.
|
||||
return r, nil
|
||||
}
|
||||
// Dynamic behavior.
|
||||
// Look for the base of the outermost schema on the stack with this dynamic
|
||||
// anchor. (Yes, outermost: the one farthest from here. This the opposite
|
||||
// of how ordinary dynamic variables behave.)
|
||||
// Why the base of the schema being validated and not the schema itself?
|
||||
// Because the base is the scope for anchors. In fact it's possible to
|
||||
// refer to a schema that is not on the stack, but a child of some base
|
||||
// on the stack.
|
||||
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
||||
for _, s := range st.stack {
|
||||
base := st.rs.resolvedInfos[s].base
|
||||
info, ok := st.rs.resolvedInfos[base].anchors[info.dynamicRefAnchor]
|
||||
if ok && info.dynamic {
|
||||
return info.schema, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("missing dynamic anchor %q", info.dynamicRefAnchor)
|
||||
}
|
||||
|
||||
// ApplyDefaults modifies an instance by applying the schema's defaults to it. If
|
||||
// a schema or sub-schema has a default, then a corresponding missing instance value
|
||||
// is set to the default.
|
||||
//
|
||||
// The JSON Schema specification does not describe how defaults should be interpreted.
|
||||
// This method honors defaults only on properties, and only those that are not required.
|
||||
// If the instance is a map and the property is missing, the property is added to
|
||||
// the map with the default.
|
||||
// ApplyDefaults does not support structs, because it cannot know whether a field
|
||||
// is missing in the JSON, or was explicitly set to its zero value.
|
||||
//
|
||||
// ApplyDefaults can panic if a default cannot be assigned to a field.
|
||||
//
|
||||
// The argument must be a pointer to the instance.
|
||||
// (In case we decide that top-level defaults are meaningful.)
|
||||
//
|
||||
// It is recommended to first call Resolve with a ValidateDefaults option of true,
|
||||
// then call this method, and lastly call Validate.
|
||||
func (rs *Resolved) ApplyDefaults(instancep any) error {
|
||||
// TODO(jba): consider what defaults on top-level or array instances might mean.
|
||||
// TODO(jba): follow $ref and $dynamicRef
|
||||
st := &state{rs: rs}
|
||||
return st.applyDefaults(reflect.ValueOf(instancep), rs.root)
|
||||
}
|
||||
|
||||
// Recursive helper used by ApplyDefaults. Applies defaults on sub-schemas
|
||||
// of object properties recursively.
|
||||
func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) {
|
||||
defer wrapf(&err, "applyDefaults: schema %s, instance %v", st.rs.schemaString(schema), instancep)
|
||||
|
||||
schemaInfo := st.rs.resolvedInfos[schema]
|
||||
instance := instancep.Elem()
|
||||
if instance.Kind() == reflect.Interface && instance.IsValid() {
|
||||
// If we unmarshalled into 'any', the default object unmarshalling will be map[string]any.
|
||||
instance = instance.Elem()
|
||||
}
|
||||
if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct {
|
||||
if instance.Kind() == reflect.Map {
|
||||
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
|
||||
return fmt.Errorf("map key type %s is not a string", kt)
|
||||
}
|
||||
}
|
||||
for prop, subschema := range schema.Properties {
|
||||
// Ignore defaults on required properties. (A required property shouldn't have a default.)
|
||||
if schemaInfo.isRequired[prop] {
|
||||
continue
|
||||
}
|
||||
val := property(instance, prop)
|
||||
switch instance.Kind() {
|
||||
case reflect.Map:
|
||||
// If there is a default for this property, and the map key is missing,
|
||||
// set the map value to the default.
|
||||
if subschema.Default != nil && !val.IsValid() {
|
||||
// Create an lvalue, since map values aren't addressable.
|
||||
lvalue := reflect.New(instance.Type().Elem())
|
||||
if err := json.Unmarshal(subschema.Default, lvalue.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
// Recurse unconditionally; applyDefaults will only act on object-like values.
|
||||
if err := st.applyDefaults(lvalue, subschema); err != nil {
|
||||
return err
|
||||
}
|
||||
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
|
||||
} else if val.IsValid() {
|
||||
// Recurse into an existing sub-instance.
|
||||
// MapIndex returns a non-addressable value; copy into an addressable lvalue, recurse, then set back.
|
||||
lvalue := reflect.New(instance.Type().Elem())
|
||||
// Initialize the lvalue with current value.
|
||||
lvalue.Elem().Set(val)
|
||||
if err := st.applyDefaults(lvalue, subschema); err != nil {
|
||||
return err
|
||||
}
|
||||
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
|
||||
} else if schemaHasDefaultsInProperties(subschema) {
|
||||
// Property is missing, but descendants still have some defaults
|
||||
// Create an empty container and recurse to populate
|
||||
elemType := instance.Type().Elem()
|
||||
var child reflect.Value
|
||||
switch elemType.Kind() {
|
||||
case reflect.Interface:
|
||||
child = reflect.ValueOf(map[string]any{})
|
||||
case reflect.Map:
|
||||
child = reflect.MakeMap(elemType)
|
||||
case reflect.Struct:
|
||||
child = reflect.New(elemType).Elem()
|
||||
}
|
||||
if child.IsValid() {
|
||||
lvalue := reflect.New(elemType)
|
||||
lvalue.Elem().Set(child)
|
||||
if err := st.applyDefaults(lvalue, subschema); err != nil {
|
||||
return err
|
||||
}
|
||||
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
return errors.New("cannot apply defaults to a struct")
|
||||
default:
|
||||
panic(fmt.Sprintf("applyDefaults: property %s: bad value %s of kind %s",
|
||||
prop, instance, instance.Kind()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// schemaHasDefaultsInProperties reports whether s or any descendant schema under
|
||||
// its Properties contains a default. Only walks Properties to match ApplyDefaults semantics.
|
||||
func schemaHasDefaultsInProperties(s *Schema) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
if s.Default != nil {
|
||||
return true
|
||||
}
|
||||
if s.Properties != nil {
|
||||
for _, ss := range s.Properties {
|
||||
if schemaHasDefaultsInProperties(ss) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// property returns the value of the property of v with the given name, or the invalid
|
||||
// reflect.Value if there is none.
|
||||
// If v is a map, the property is the value of the map whose key is name.
|
||||
// If v is a struct, the property is the value of the field with the given name according
|
||||
// to the encoding/json package (see [jsonName]).
|
||||
// If v is anything else, property panics.
|
||||
func property(v reflect.Value, name string) reflect.Value {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
return v.MapIndex(reflect.ValueOf(name))
|
||||
case reflect.Struct:
|
||||
props := structPropertiesOf(v.Type())
|
||||
// Ignore nonexistent properties.
|
||||
if sf, ok := props[name]; ok {
|
||||
return v.FieldByIndex(sf.Index)
|
||||
}
|
||||
return reflect.Value{}
|
||||
default:
|
||||
panic(fmt.Sprintf("property(%q): bad value %s of kind %s", name, v, v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// properties returns an iterator over the names and values of all properties
|
||||
// in v, which must be a map or a struct.
|
||||
// If a struct, zero-valued properties that are marked omitempty or omitzero
|
||||
// are excluded.
|
||||
func properties(v reflect.Value) iter.Seq2[string, reflect.Value] {
|
||||
return func(yield func(string, reflect.Value) bool) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
for k, e := range v.Seq2() {
|
||||
if !yield(k.String(), e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
for name, sf := range structPropertiesOf(v.Type()) {
|
||||
val := v.FieldByIndex(sf.Index)
|
||||
if val.IsZero() {
|
||||
info := fieldJSONInfo(sf)
|
||||
if info.settings["omitempty"] || info.settings["omitzero"] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !yield(name, val) {
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("bad value %s of kind %s", v, v.Kind()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// numPropertiesBounds returns bounds on the number of v's properties.
|
||||
// v must be a map or a struct.
|
||||
// If v is a map, both bounds are the map's size.
|
||||
// If v is a struct, the max is the number of struct properties.
|
||||
// But since we don't know whether a zero value indicates a missing optional property
|
||||
// or not, be generous and use the number of non-zero properties as the min.
|
||||
func numPropertiesBounds(v reflect.Value, isRequired map[string]bool) (int, int) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
return v.Len(), v.Len()
|
||||
case reflect.Struct:
|
||||
sp := structPropertiesOf(v.Type())
|
||||
min := 0
|
||||
for prop, sf := range sp {
|
||||
if !v.FieldByIndex(sf.Index).IsZero() || isRequired[prop] {
|
||||
min++
|
||||
}
|
||||
}
|
||||
return min, len(sp)
|
||||
default:
|
||||
panic(fmt.Sprintf("properties: bad value: %s of kind %s", v, v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// A propertyMap is a map from property name to struct field index.
|
||||
type propertyMap = map[string]reflect.StructField
|
||||
|
||||
var structProperties sync.Map // from reflect.Type to propertyMap
|
||||
|
||||
// structPropertiesOf returns the JSON Schema properties for the struct type t.
|
||||
// The caller must not mutate the result.
|
||||
func structPropertiesOf(t reflect.Type) propertyMap {
|
||||
// Mutex not necessary: at worst we'll recompute the same value.
|
||||
if props, ok := structProperties.Load(t); ok {
|
||||
return props.(propertyMap)
|
||||
}
|
||||
props := map[string]reflect.StructField{}
|
||||
for _, sf := range reflect.VisibleFields(t) {
|
||||
if sf.Anonymous {
|
||||
continue
|
||||
}
|
||||
info := fieldJSONInfo(sf)
|
||||
if !info.omit {
|
||||
props[info.name] = sf
|
||||
}
|
||||
}
|
||||
structProperties.Store(t, props)
|
||||
return props
|
||||
}
|
||||
Reference in New Issue
Block a user