fix(go.sum): update ResolveSpec dependency to v1.0.87
CI / build-and-test (push) Failing after 1s
Release / release (push) Failing after 19m26s

This commit is contained in:
Hein
2026-06-23 13:17:16 +02:00
parent 0227912325
commit 1adf50e3db
2436 changed files with 1078758 additions and 114 deletions
+76
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}