jsonschema: make schema state independent of resolve arguments (#85)
Move schema state that depends on resolution into the Resolved struct. The remaining unexported schema fields are dependent only on the schema itself and its sub-schemas. Now a schema can be resolved multiple times, as itself or as part of other schemas. Fixes #84.
This commit is contained in:
parent
dced3e4503
commit
3bbe74f8a5
4 changed files with 171 additions and 136 deletions
|
|
@ -25,12 +25,77 @@ type Resolved struct {
|
||||||
root *Schema
|
root *Schema
|
||||||
// map from $ids to their schemas
|
// map from $ids to their schemas
|
||||||
resolvedURIs map[string]*Schema
|
resolvedURIs map[string]*Schema
|
||||||
|
// map from schemas to additional info computed during resolution
|
||||||
|
resolvedInfos map[*Schema]*resolvedInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResolved(s *Schema) *Resolved {
|
||||||
|
return &Resolved{
|
||||||
|
root: s,
|
||||||
|
resolvedURIs: map[string]*Schema{},
|
||||||
|
resolvedInfos: map[*Schema]*resolvedInfo{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Schema returns the schema that was resolved.
|
||||||
// It must not be modified.
|
// It must not be modified.
|
||||||
func (r *Resolved) Schema() *Schema { return r.root }
|
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.
|
// A Loader reads and unmarshals the schema at uri, if any.
|
||||||
type Loader func(uri *url.URL) (*Schema, error)
|
type Loader func(uri *url.URL) (*Schema, error)
|
||||||
|
|
||||||
|
|
@ -59,6 +124,8 @@ type ResolveOptions struct {
|
||||||
// Resolve resolves all references within the schema and performs other tasks that
|
// Resolve resolves all references within the schema and performs other tasks that
|
||||||
// prepare the schema for validation.
|
// prepare the schema for validation.
|
||||||
// If opts is nil, the default values are used.
|
// 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) {
|
func (root *Schema) Resolve(opts *ResolveOptions) (*Resolved, error) {
|
||||||
// There are up to five steps required to prepare a schema to validate.
|
// There are up to five steps required to prepare a schema to validate.
|
||||||
// 1. Load: read the schema from somewhere and unmarshal it.
|
// 1. Load: read the schema from somewhere and unmarshal it.
|
||||||
|
|
@ -71,9 +138,6 @@ func (root *Schema) Resolve(opts *ResolveOptions) (*Resolved, error) {
|
||||||
// in a map from URIs to schemas within root.
|
// 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.
|
// 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.
|
// 5. (Optional.) If opts.ValidateDefaults is true, validate the defaults.
|
||||||
if root.path != "" {
|
|
||||||
return nil, fmt.Errorf("jsonschema: Resolve: %s already resolved", root)
|
|
||||||
}
|
|
||||||
r := &resolver{loaded: map[string]*Resolved{}}
|
r := &resolver{loaded: map[string]*Resolved{}}
|
||||||
if opts != nil {
|
if opts != nil {
|
||||||
r.opts = *opts
|
r.opts = *opts
|
||||||
|
|
@ -121,20 +185,21 @@ func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
|
||||||
if baseURI.Fragment != "" {
|
if baseURI.Fragment != "" {
|
||||||
return nil, fmt.Errorf("base URI %s must not have a fragment", baseURI)
|
return nil, fmt.Errorf("base URI %s must not have a fragment", baseURI)
|
||||||
}
|
}
|
||||||
if err := s.check(); err != nil {
|
rs := newResolved(s)
|
||||||
|
|
||||||
|
if err := s.check(rs.resolvedInfos); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := resolveURIs(s, baseURI)
|
if err := resolveURIs(rs, baseURI); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rs := &Resolved{root: s, resolvedURIs: m}
|
|
||||||
// Remember the schema by both the URI we loaded it from and its canonical name,
|
// Remember the schema by both the URI we loaded it from and its canonical name,
|
||||||
// which may differ if the schema has an $id.
|
// which may differ if the schema has an $id.
|
||||||
// We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
|
// We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
|
||||||
r.loaded[baseURI.String()] = rs
|
r.loaded[baseURI.String()] = rs
|
||||||
r.loaded[s.uri.String()] = rs
|
r.loaded[rs.resolvedInfos[s].uri.String()] = rs
|
||||||
|
|
||||||
if err := r.resolveRefs(rs); err != nil {
|
if err := r.resolveRefs(rs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -142,10 +207,10 @@ func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
|
||||||
return rs, nil
|
return rs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (root *Schema) check() error {
|
func (root *Schema) check(infos map[*Schema]*resolvedInfo) error {
|
||||||
// Check for structural validity. Do this first and fail fast:
|
// Check for structural validity. Do this first and fail fast:
|
||||||
// bad structure will cause other code to panic.
|
// bad structure will cause other code to panic.
|
||||||
if err := root.checkStructure(); err != nil {
|
if err := root.checkStructure(infos); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,14 +218,16 @@ func (root *Schema) check() error {
|
||||||
report := func(err error) { errs = append(errs, err) }
|
report := func(err error) { errs = append(errs, err) }
|
||||||
|
|
||||||
for ss := range root.all() {
|
for ss := range root.all() {
|
||||||
ss.checkLocal(report)
|
ss.checkLocal(report, infos)
|
||||||
}
|
}
|
||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkStructure verifies that root and its subschemas form a tree.
|
// checkStructure verifies that root and its subschemas form a tree.
|
||||||
// It also assigns each schema a unique path, to improve error messages.
|
// It also assigns each schema a unique path, to improve error messages.
|
||||||
func (root *Schema) checkStructure() error {
|
func (root *Schema) checkStructure(infos map[*Schema]*resolvedInfo) error {
|
||||||
|
assert(len(infos) == 0, "non-empty infos")
|
||||||
|
|
||||||
var check func(reflect.Value, []byte) error
|
var check func(reflect.Value, []byte) error
|
||||||
check = func(v reflect.Value, path []byte) error {
|
check = func(v reflect.Value, path []byte) error {
|
||||||
// For the purpose of error messages, the root schema has path "root"
|
// For the purpose of error messages, the root schema has path "root"
|
||||||
|
|
@ -173,16 +240,15 @@ func (root *Schema) checkStructure() error {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return fmt.Errorf("jsonschema: schema at %s is nil", p)
|
return fmt.Errorf("jsonschema: schema at %s is nil", p)
|
||||||
}
|
}
|
||||||
if s.path != "" {
|
if info, ok := infos[s]; ok {
|
||||||
// We've seen s before.
|
// We've seen s before.
|
||||||
// The schema graph at root is not a tree, but it needs to
|
// The schema graph at root is not a tree, but it needs to
|
||||||
// be because we assume a unique parent when we store a schema's base
|
// be because a schema's base must be unique.
|
||||||
// in the Schema. A cycle would also put Schema.all into an infinite
|
// A cycle would also put Schema.all into an infinite recursion.
|
||||||
// recursion.
|
|
||||||
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
|
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
|
||||||
root, s.path, p)
|
root, info.path, p)
|
||||||
}
|
}
|
||||||
s.path = p
|
infos[s] = &resolvedInfo{s: s, path: p}
|
||||||
|
|
||||||
for _, info := range schemaFieldInfos {
|
for _, info := range schemaFieldInfos {
|
||||||
fv := v.Elem().FieldByIndex(info.sf.Index)
|
fv := v.Elem().FieldByIndex(info.sf.Index)
|
||||||
|
|
@ -224,7 +290,7 @@ func (root *Schema) checkStructure() error {
|
||||||
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
|
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
|
||||||
// in the schema for later use.
|
// in the schema for later use.
|
||||||
// It appends the errors it finds to errs.
|
// It appends the errors it finds to errs.
|
||||||
func (s *Schema) checkLocal(report func(error)) {
|
func (s *Schema) checkLocal(report func(error), infos map[*Schema]*resolvedInfo) {
|
||||||
addf := func(format string, args ...any) {
|
addf := func(format string, args ...any) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
report(fmt.Errorf("jsonschema.Schema: %s: %s", s, msg))
|
report(fmt.Errorf("jsonschema.Schema: %s: %s", s, msg))
|
||||||
|
|
@ -250,33 +316,35 @@ func (s *Schema) checkLocal(report func(error)) {
|
||||||
addf("cannot validate a schema with $vocabulary")
|
addf("cannot validate a schema with $vocabulary")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info := infos[s]
|
||||||
|
|
||||||
// Check and compile regexps.
|
// Check and compile regexps.
|
||||||
if s.Pattern != "" {
|
if s.Pattern != "" {
|
||||||
re, err := regexp.Compile(s.Pattern)
|
re, err := regexp.Compile(s.Pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
addf("pattern: %v", err)
|
addf("pattern: %v", err)
|
||||||
} else {
|
} else {
|
||||||
s.pattern = re
|
info.pattern = re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(s.PatternProperties) > 0 {
|
if len(s.PatternProperties) > 0 {
|
||||||
s.patternProperties = map[*regexp.Regexp]*Schema{}
|
info.patternProperties = map[*regexp.Regexp]*Schema{}
|
||||||
for reString, subschema := range s.PatternProperties {
|
for reString, subschema := range s.PatternProperties {
|
||||||
re, err := regexp.Compile(reString)
|
re, err := regexp.Compile(reString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
addf("patternProperties[%q]: %v", reString, err)
|
addf("patternProperties[%q]: %v", reString, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.patternProperties[re] = subschema
|
info.patternProperties[re] = subschema
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a set of required properties, to avoid quadratic behavior when validating
|
// Build a set of required properties, to avoid quadratic behavior when validating
|
||||||
// a struct.
|
// a struct.
|
||||||
if len(s.Required) > 0 {
|
if len(s.Required) > 0 {
|
||||||
s.isRequired = map[string]bool{}
|
info.isRequired = map[string]bool{}
|
||||||
for _, r := range s.Required {
|
for _, r := range s.Required {
|
||||||
s.isRequired[r] = true
|
info.isRequired[r] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -285,8 +353,6 @@ func (s *Schema) checkLocal(report func(error)) {
|
||||||
// to baseURI.
|
// to baseURI.
|
||||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
|
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
|
||||||
// 8.2.1.
|
// 8.2.1.
|
||||||
|
|
||||||
// TODO(jba): dynamicAnchors (§8.2.2)
|
|
||||||
//
|
//
|
||||||
// Every schema has a base URI and a parent base URI.
|
// Every schema has a base URI and a parent base URI.
|
||||||
//
|
//
|
||||||
|
|
@ -316,11 +382,12 @@ func (s *Schema) checkLocal(report func(error)) {
|
||||||
// allOf/1 http://b.com (absolute $id; doesn't matter that it's not under the loaded URI)
|
// 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 http://a.com/root.json (inherited from parent)
|
||||||
// allOf/2/not http://a.com/root.json (inherited from parent)
|
// allOf/2/not http://a.com/root.json (inherited from parent)
|
||||||
func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
|
func resolveURIs(rs *Resolved, baseURI *url.URL) error {
|
||||||
resolvedURIs := map[string]*Schema{}
|
|
||||||
|
|
||||||
var resolve func(s, base *Schema) error
|
var resolve func(s, base *Schema) error
|
||||||
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.
|
// ids are scoped to the root.
|
||||||
if s.ID != "" {
|
if s.ID != "" {
|
||||||
// A non-empty ID establishes a new base.
|
// A non-empty ID establishes a new base.
|
||||||
|
|
@ -332,26 +399,27 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
|
||||||
return fmt.Errorf("$id %s must not have a fragment", s.ID)
|
return fmt.Errorf("$id %s must not have a fragment", s.ID)
|
||||||
}
|
}
|
||||||
// The base URI for this schema is its $id resolved against the parent base.
|
// The base URI for this schema is its $id resolved against the parent base.
|
||||||
s.uri = base.uri.ResolveReference(idURI)
|
info.uri = baseInfo.uri.ResolveReference(idURI)
|
||||||
if !s.uri.IsAbs() {
|
if !info.uri.IsAbs() {
|
||||||
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %s)", s.ID, s.base.uri)
|
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %q)", s.ID, baseInfo.uri)
|
||||||
}
|
}
|
||||||
resolvedURIs[s.uri.String()] = s
|
rs.resolvedURIs[info.uri.String()] = s
|
||||||
base = s // needed for anchors
|
base = s // needed for anchors
|
||||||
|
baseInfo = rs.resolvedInfos[base]
|
||||||
}
|
}
|
||||||
s.base = base
|
info.base = base
|
||||||
|
|
||||||
// Anchors and dynamic anchors are URI fragments that are scoped to their base.
|
// 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.
|
// We treat them as keys in a map stored within the schema.
|
||||||
setAnchor := func(anchor string, dynamic bool) error {
|
setAnchor := func(anchor string, dynamic bool) error {
|
||||||
if anchor != "" {
|
if anchor != "" {
|
||||||
if _, ok := base.anchors[anchor]; ok {
|
if _, ok := baseInfo.anchors[anchor]; ok {
|
||||||
return fmt.Errorf("duplicate anchor %q in %s", anchor, base.uri)
|
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseInfo.uri)
|
||||||
}
|
}
|
||||||
if base.anchors == nil {
|
if baseInfo.anchors == nil {
|
||||||
base.anchors = map[string]anchorInfo{}
|
baseInfo.anchors = map[string]anchorInfo{}
|
||||||
}
|
}
|
||||||
base.anchors[anchor] = anchorInfo{s, dynamic}
|
baseInfo.anchors[anchor] = anchorInfo{s, dynamic}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -368,13 +436,11 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the root URI to the base for now. If the root has an $id, this will change.
|
// Set the root URI to the base for now. If the root has an $id, this will change.
|
||||||
root.uri = baseURI
|
rs.resolvedInfos[rs.root].uri = baseURI
|
||||||
// The original base, even if changed, is still a valid way to refer to the root.
|
// The original base, even if changed, is still a valid way to refer to the root.
|
||||||
resolvedURIs[baseURI.String()] = root
|
rs.resolvedURIs[baseURI.String()] = rs.root
|
||||||
if err := resolve(root, root); err != nil {
|
|
||||||
return nil, err
|
return resolve(rs.root, rs.root)
|
||||||
}
|
|
||||||
return resolvedURIs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveRefs replaces every ref in the schemas with the schema it refers to.
|
// resolveRefs replaces every ref in the schemas with the schema it refers to.
|
||||||
|
|
@ -382,6 +448,7 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
|
||||||
// that needs to be loaded.
|
// that needs to be loaded.
|
||||||
func (r *resolver) resolveRefs(rs *Resolved) error {
|
func (r *resolver) resolveRefs(rs *Resolved) error {
|
||||||
for s := range rs.root.all() {
|
for s := range rs.root.all() {
|
||||||
|
info := rs.resolvedInfos[s]
|
||||||
if s.Ref != "" {
|
if s.Ref != "" {
|
||||||
refSchema, _, err := r.resolveRef(rs, s, s.Ref)
|
refSchema, _, err := r.resolveRef(rs, s, s.Ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -389,7 +456,7 @@ func (r *resolver) resolveRefs(rs *Resolved) error {
|
||||||
}
|
}
|
||||||
// Whether or not the anchor referred to by $ref fragment is dynamic,
|
// Whether or not the anchor referred to by $ref fragment is dynamic,
|
||||||
// the ref still treats it lexically.
|
// the ref still treats it lexically.
|
||||||
s.resolvedRef = refSchema
|
info.resolvedRef = refSchema
|
||||||
}
|
}
|
||||||
if s.DynamicRef != "" {
|
if s.DynamicRef != "" {
|
||||||
refSchema, frag, err := r.resolveRef(rs, s, s.DynamicRef)
|
refSchema, frag, err := r.resolveRef(rs, s, s.DynamicRef)
|
||||||
|
|
@ -399,11 +466,11 @@ func (r *resolver) resolveRefs(rs *Resolved) error {
|
||||||
if frag != "" {
|
if frag != "" {
|
||||||
// The dynamic ref's fragment points to a dynamic anchor.
|
// The dynamic ref's fragment points to a dynamic anchor.
|
||||||
// We must resolve the fragment at validation time.
|
// We must resolve the fragment at validation time.
|
||||||
s.dynamicRefAnchor = frag
|
info.dynamicRefAnchor = frag
|
||||||
} else {
|
} else {
|
||||||
// There is no dynamic anchor in the lexically referenced schema,
|
// There is no dynamic anchor in the lexically referenced schema,
|
||||||
// so the dynamic ref behaves like a lexical ref.
|
// so the dynamic ref behaves like a lexical ref.
|
||||||
s.resolvedDynamicRef = refSchema
|
info.resolvedDynamicRef = refSchema
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -417,7 +484,8 @@ func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, d
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
// URI-resolve the ref against the current base URI to get a complete URI.
|
// URI-resolve the ref against the current base URI to get a complete URI.
|
||||||
refURI = s.base.uri.ResolveReference(refURI)
|
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.
|
// 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
|
// This part is the same for dynamic refs too: their non-fragment part resolves
|
||||||
// lexically.
|
// lexically.
|
||||||
|
|
@ -447,6 +515,13 @@ func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, d
|
||||||
}
|
}
|
||||||
referencedSchema = lrs.root
|
referencedSchema = lrs.root
|
||||||
assert(referencedSchema != nil, "nil referenced schema")
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,7 +531,9 @@ func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, d
|
||||||
// A JSON Pointer is either the empty string or begins with a '/',
|
// A JSON Pointer is either the empty string or begins with a '/',
|
||||||
// whereas anchors are always non-empty strings that don't contain slashes.
|
// whereas anchors are always non-empty strings that don't contain slashes.
|
||||||
if frag != "" && !strings.HasPrefix(frag, "/") {
|
if frag != "" && !strings.HasPrefix(frag, "/") {
|
||||||
info, found := referencedSchema.anchors[frag]
|
resInfo := rs.resolvedInfos[referencedSchema]
|
||||||
|
info, found := resInfo.anchors[frag]
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return nil, "", fmt.Errorf("no anchor %q in %s", frag, s)
|
return nil, "", fmt.Errorf("no anchor %q in %s", frag, s)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ import (
|
||||||
func TestSchemaStructure(t *testing.T) {
|
func TestSchemaStructure(t *testing.T) {
|
||||||
check := func(s *Schema, want string) {
|
check := func(s *Schema, want string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
err := s.checkStructure()
|
infos := map[*Schema]*resolvedInfo{}
|
||||||
|
err := s.checkStructure(infos)
|
||||||
if err == nil || !strings.Contains(err.Error(), want) {
|
if err == nil || !strings.Contains(err.Error(), want) {
|
||||||
t.Errorf("checkStructure returned error %q, want %q", err, want)
|
t.Errorf("checkStructure returned error %q, want %q", err, want)
|
||||||
}
|
}
|
||||||
|
|
@ -89,13 +90,14 @@ func TestPaths(t *testing.T) {
|
||||||
{root.PrefixItems[1], "/prefixItems/1"},
|
{root.PrefixItems[1], "/prefixItems/1"},
|
||||||
{root.PrefixItems[1].Items, "/prefixItems/1/items"},
|
{root.PrefixItems[1].Items, "/prefixItems/1/items"},
|
||||||
}
|
}
|
||||||
if err := root.checkStructure(); err != nil {
|
rs := newResolved(root)
|
||||||
|
if err := root.checkStructure(rs.resolvedInfos); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var got []item
|
var got []item
|
||||||
for s := range root.all() {
|
for s := range root.all() {
|
||||||
got = append(got, item{s, s.path})
|
got = append(got, item{s, rs.resolvedInfos[s].path})
|
||||||
}
|
}
|
||||||
if !slices.Equal(got, want) {
|
if !slices.Equal(got, want) {
|
||||||
t.Errorf("\ngot %v\nwant %v", got, want)
|
t.Errorf("\ngot %v\nwant %v", got, want)
|
||||||
|
|
@ -129,8 +131,12 @@ func TestResolveURIs(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
got, err := resolveURIs(root, base)
|
|
||||||
if err != nil {
|
rs := newResolved(root)
|
||||||
|
if err := root.check(rs.resolvedInfos); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := resolveURIs(rs, base); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,6 +160,7 @@ func TestResolveURIs(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
got := rs.resolvedURIs
|
||||||
gotKeys := slices.Sorted(maps.Keys(got))
|
gotKeys := slices.Sorted(maps.Keys(got))
|
||||||
wantKeys := slices.Sorted(maps.Keys(wantIDs))
|
wantKeys := slices.Sorted(maps.Keys(wantIDs))
|
||||||
if !slices.Equal(gotKeys, wantKeys) {
|
if !slices.Equal(gotKeys, wantKeys) {
|
||||||
|
|
@ -163,11 +170,12 @@ func TestResolveURIs(t *testing.T) {
|
||||||
t.Errorf("IDs:\ngot %+v\n\nwant %+v", got, wantIDs)
|
t.Errorf("IDs:\ngot %+v\n\nwant %+v", got, wantIDs)
|
||||||
}
|
}
|
||||||
for s := range root.all() {
|
for s := range root.all() {
|
||||||
|
info := rs.resolvedInfos[s]
|
||||||
if want := wantAnchors[s]; want != nil {
|
if want := wantAnchors[s]; want != nil {
|
||||||
if got := s.anchors; !maps.Equal(got, want) {
|
if got := info.anchors; !maps.Equal(got, want) {
|
||||||
t.Errorf("anchors:\ngot %+v\n\nwant %+v", got, want)
|
t.Errorf("anchors:\ngot %+v\n\nwant %+v", got, want)
|
||||||
}
|
}
|
||||||
} else if s.anchors != nil {
|
} else if info.anchors != nil {
|
||||||
t.Errorf("non-nil anchors for %s", s)
|
t.Errorf("non-nil anchors for %s", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +207,7 @@ func TestRefCycle(t *testing.T) {
|
||||||
|
|
||||||
check := func(s *Schema, key string) {
|
check := func(s *Schema, key string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if s.resolvedRef != schemas[key] {
|
if rs.resolvedInfos[s].resolvedRef != schemas[key] {
|
||||||
t.Errorf("%s resolvedRef != schemas[%q]", s.json(), key)
|
t.Errorf("%s resolvedRef != schemas[%q]", s.json(), key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,7 @@ import (
|
||||||
"iter"
|
"iter"
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/modelcontextprotocol/go-sdk/internal/util"
|
"github.com/modelcontextprotocol/go-sdk/internal/util"
|
||||||
|
|
@ -129,47 +127,6 @@ type Schema struct {
|
||||||
|
|
||||||
// Extra allows for additional keywords beyond those specified.
|
// Extra allows for additional keywords beyond those specified.
|
||||||
Extra map[string]any `json:"-"`
|
Extra map[string]any `json:"-"`
|
||||||
|
|
||||||
// computed fields
|
|
||||||
|
|
||||||
// This 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 JSON Pointer path from the root schema to here.
|
|
||||||
// Used in errors.
|
|
||||||
path string
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// Map from anchors to subschemas.
|
|
||||||
anchors map[string]anchorInfo
|
|
||||||
|
|
||||||
// compiled regexps
|
|
||||||
pattern *regexp.Regexp
|
|
||||||
patternProperties map[*regexp.Regexp]*Schema
|
|
||||||
|
|
||||||
// the set of required properties
|
|
||||||
isRequired map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// falseSchema returns a new Schema tree that fails to validate any value.
|
// falseSchema returns a new Schema tree that fails to validate any value.
|
||||||
|
|
@ -186,28 +143,15 @@ type anchorInfo struct {
|
||||||
|
|
||||||
// String returns a short description of the schema.
|
// String returns a short description of the schema.
|
||||||
func (s *Schema) String() string {
|
func (s *Schema) String() string {
|
||||||
if s.uri != nil {
|
if s.ID != "" {
|
||||||
if u := s.uri.String(); u != "" {
|
return s.ID
|
||||||
return u
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" {
|
if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" {
|
||||||
return fmt.Sprintf("%q, anchor %s", s.base.uri.String(), a)
|
return fmt.Sprintf("anchor %s", a)
|
||||||
}
|
|
||||||
if s.path != "" {
|
|
||||||
return s.path
|
|
||||||
}
|
}
|
||||||
return "<anonymous schema>"
|
return "<anonymous schema>"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedRef returns the Schema to which this schema's $ref keyword
|
|
||||||
// refers, or nil if it doesn't have a $ref.
|
|
||||||
// It returns nil if this schema has not been resolved, meaning that
|
|
||||||
// [Schema.Resolve] was called on it or one of its ancestors.
|
|
||||||
func (s *Schema) ResolvedRef() *Schema {
|
|
||||||
return s.resolvedRef
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Schema) basicChecks() error {
|
func (s *Schema) basicChecks() error {
|
||||||
if s.Type != "" && s.Types != nil {
|
if s.Type != "" && s.Types != nil {
|
||||||
return errors.New("both Type and Types are set; at most one should be")
|
return errors.New("both Type and Types are set; at most one should be")
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@ func (rs *Resolved) validateDefaults() error {
|
||||||
// We checked for nil schemas in [Schema.Resolve].
|
// We checked for nil schemas in [Schema.Resolve].
|
||||||
assert(s != nil, "nil schema")
|
assert(s != nil, "nil schema")
|
||||||
if s.DynamicRef != "" {
|
if s.DynamicRef != "" {
|
||||||
return fmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", s)
|
return fmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", rs.schemaString(s))
|
||||||
}
|
}
|
||||||
if s.Default != nil {
|
if s.Default != nil {
|
||||||
var d any
|
var d any
|
||||||
if err := json.Unmarshal(s.Default, &d); err != nil {
|
if err := json.Unmarshal(s.Default, &d); err != nil {
|
||||||
return fmt.Errorf("unmarshaling default value of schema %s: %w", s, err)
|
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 {
|
if err := st.validate(reflect.ValueOf(d), s, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -74,7 +74,7 @@ type state struct {
|
||||||
|
|
||||||
// validate validates the reflected value of the instance.
|
// validate validates the reflected value of the instance.
|
||||||
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) {
|
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) {
|
||||||
defer util.Wrapf(&err, "validating %s", schema)
|
defer util.Wrapf(&err, "validating %s", st.rs.schemaString(schema))
|
||||||
|
|
||||||
// Maintain a stack for dynamic schema resolution.
|
// Maintain a stack for dynamic schema resolution.
|
||||||
st.stack = append(st.stack, schema) // push
|
st.stack = append(st.stack, schema) // push
|
||||||
|
|
@ -90,6 +90,8 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
instance = instance.Elem()
|
instance = instance.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schemaInfo := st.rs.resolvedInfos[schema]
|
||||||
|
|
||||||
// type: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1
|
// type: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1
|
||||||
if schema.Type != "" || schema.Types != nil {
|
if schema.Type != "" || schema.Types != nil {
|
||||||
gotType, ok := jsonType(instance)
|
gotType, ok := jsonType(instance)
|
||||||
|
|
@ -176,7 +178,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if schema.Pattern != "" && !schema.pattern.MatchString(str) {
|
if schema.Pattern != "" && !schemaInfo.pattern.MatchString(str) {
|
||||||
return fmt.Errorf("pattern: %q does not match regular expression %q", str, schema.Pattern)
|
return fmt.Errorf("pattern: %q does not match regular expression %q", str, schema.Pattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +187,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
|
|
||||||
// $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1
|
// $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1
|
||||||
if schema.Ref != "" {
|
if schema.Ref != "" {
|
||||||
if err := st.validate(instance, schema.resolvedRef, &anns); err != nil {
|
if err := st.validate(instance, schemaInfo.resolvedRef, &anns); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,11 +195,11 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
// $dynamicRef: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2
|
// $dynamicRef: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2
|
||||||
if schema.DynamicRef != "" {
|
if schema.DynamicRef != "" {
|
||||||
// The ref behaves lexically or dynamically, but not both.
|
// The ref behaves lexically or dynamically, but not both.
|
||||||
assert((schema.resolvedDynamicRef == nil) != (schema.dynamicRefAnchor == ""),
|
assert((schemaInfo.resolvedDynamicRef == nil) != (schemaInfo.dynamicRefAnchor == ""),
|
||||||
"DynamicRef not resolved properly")
|
"DynamicRef not resolved properly")
|
||||||
if schema.resolvedDynamicRef != nil {
|
if schemaInfo.resolvedDynamicRef != nil {
|
||||||
// Same as $ref.
|
// Same as $ref.
|
||||||
if err := st.validate(instance, schema.resolvedDynamicRef, &anns); err != nil {
|
if err := st.validate(instance, schemaInfo.resolvedDynamicRef, &anns); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -212,14 +214,15 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
||||||
var dynamicSchema *Schema
|
var dynamicSchema *Schema
|
||||||
for _, s := range st.stack {
|
for _, s := range st.stack {
|
||||||
info, ok := s.base.anchors[schema.dynamicRefAnchor]
|
base := st.rs.resolvedInfos[s].base
|
||||||
|
info, ok := st.rs.resolvedInfos[base].anchors[schemaInfo.dynamicRefAnchor]
|
||||||
if ok && info.dynamic {
|
if ok && info.dynamic {
|
||||||
dynamicSchema = info.schema
|
dynamicSchema = info.schema
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dynamicSchema == nil {
|
if dynamicSchema == nil {
|
||||||
return fmt.Errorf("missing dynamic anchor %q", schema.dynamicRefAnchor)
|
return fmt.Errorf("missing dynamic anchor %q", schemaInfo.dynamicRefAnchor)
|
||||||
}
|
}
|
||||||
if err := st.validate(instance, dynamicSchema, &anns); err != nil {
|
if err := st.validate(instance, dynamicSchema, &anns); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -417,7 +420,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
// If the instance is a struct and an optional property has the zero
|
// 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:
|
// value, then we could interpret it as present or missing. Be generous:
|
||||||
// assume it's missing, and thus always validates successfully.
|
// assume it's missing, and thus always validates successfully.
|
||||||
if instance.Kind() == reflect.Struct && val.IsZero() && !schema.isRequired[prop] {
|
if instance.Kind() == reflect.Struct && val.IsZero() && !schemaInfo.isRequired[prop] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := st.validate(val, subschema, nil); err != nil {
|
if err := st.validate(val, subschema, nil); err != nil {
|
||||||
|
|
@ -428,7 +431,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
if len(schema.PatternProperties) > 0 {
|
if len(schema.PatternProperties) > 0 {
|
||||||
for prop, val := range properties(instance) {
|
for prop, val := range properties(instance) {
|
||||||
// Check every matching pattern.
|
// Check every matching pattern.
|
||||||
for re, schema := range schema.patternProperties {
|
for re, schema := range schemaInfo.patternProperties {
|
||||||
if re.MatchString(prop) {
|
if re.MatchString(prop) {
|
||||||
if err := st.validate(val, schema, nil); err != nil {
|
if err := st.validate(val, schema, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -463,7 +466,7 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
|
||||||
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5
|
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5
|
||||||
var min, max int
|
var min, max int
|
||||||
if schema.MinProperties != nil || schema.MaxProperties != nil {
|
if schema.MinProperties != nil || schema.MaxProperties != nil {
|
||||||
min, max = numPropertiesBounds(instance, schema.isRequired)
|
min, max = numPropertiesBounds(instance, schemaInfo.isRequired)
|
||||||
}
|
}
|
||||||
if schema.MinProperties != nil {
|
if schema.MinProperties != nil {
|
||||||
if n, m := max, *schema.MinProperties; n < m {
|
if n, m := max, *schema.MinProperties; n < m {
|
||||||
|
|
@ -554,10 +557,11 @@ func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
|
||||||
if schema.DynamicRef == "" {
|
if schema.DynamicRef == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
info := st.rs.resolvedInfos[schema]
|
||||||
// The ref behaves lexically or dynamically, but not both.
|
// The ref behaves lexically or dynamically, but not both.
|
||||||
assert((schema.resolvedDynamicRef == nil) != (schema.dynamicRefAnchor == ""),
|
assert((info.resolvedDynamicRef == nil) != (info.dynamicRefAnchor == ""),
|
||||||
"DynamicRef not statically resolved properly")
|
"DynamicRef not statically resolved properly")
|
||||||
if r := schema.resolvedDynamicRef; r != nil {
|
if r := info.resolvedDynamicRef; r != nil {
|
||||||
// Same as $ref.
|
// Same as $ref.
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
@ -571,12 +575,13 @@ func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
|
||||||
// on the stack.
|
// on the stack.
|
||||||
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
|
||||||
for _, s := range st.stack {
|
for _, s := range st.stack {
|
||||||
info, ok := s.base.anchors[schema.dynamicRefAnchor]
|
base := st.rs.resolvedInfos[s].base
|
||||||
|
info, ok := st.rs.resolvedInfos[base].anchors[info.dynamicRefAnchor]
|
||||||
if ok && info.dynamic {
|
if ok && info.dynamic {
|
||||||
return info.schema, nil
|
return info.schema, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("missing dynamic anchor %q", schema.dynamicRefAnchor)
|
return nil, fmt.Errorf("missing dynamic anchor %q", info.dynamicRefAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyDefaults modifies an instance by applying the schema's defaults to it. If
|
// ApplyDefaults modifies an instance by applying the schema's defaults to it. If
|
||||||
|
|
@ -608,8 +613,9 @@ func (rs *Resolved) ApplyDefaults(instancep any) error {
|
||||||
// Leave this as a potentially recursive helper function, because we'll surely want
|
// Leave this as a potentially recursive helper function, because we'll surely want
|
||||||
// to apply defaults on sub-schemas someday.
|
// to apply defaults on sub-schemas someday.
|
||||||
func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) {
|
func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) {
|
||||||
defer util.Wrapf(&err, "applyDefaults: schema %s, instance %v", schema, instancep)
|
defer util.Wrapf(&err, "applyDefaults: schema %s, instance %v", st.rs.schemaString(schema), instancep)
|
||||||
|
|
||||||
|
schemaInfo := st.rs.resolvedInfos[schema]
|
||||||
instance := instancep.Elem()
|
instance := instancep.Elem()
|
||||||
if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct {
|
if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct {
|
||||||
if instance.Kind() == reflect.Map {
|
if instance.Kind() == reflect.Map {
|
||||||
|
|
@ -619,7 +625,7 @@ func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err err
|
||||||
}
|
}
|
||||||
for prop, subschema := range schema.Properties {
|
for prop, subschema := range schema.Properties {
|
||||||
// Ignore defaults on required properties. (A required property shouldn't have a default.)
|
// Ignore defaults on required properties. (A required property shouldn't have a default.)
|
||||||
if schema.isRequired[prop] {
|
if schemaInfo.isRequired[prop] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val := property(instance, prop)
|
val := property(instance, prop)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue