@@ -32,6 +32,19 @@ type Resolved struct {
3232// resolvedInfo holds information specific to a schema that is computed by [Schema.Resolve].
3333type resolvedInfo struct {
3434 s * Schema
35+ // The schema's base schema.
36+ // If the schema is the root or has an ID, its base is itself.
37+ // Otherwise, its base is the innermost enclosing schema whose base
38+ // is itself.
39+ // Intuitively, a base schema is one that can be referred to with a
40+ // fragmentless URI.
41+ base * Schema
42+ // The URI for the schema, if it is the root or has an ID.
43+ // Otherwise nil.
44+ // Invariants:
45+ // s.base.uri != nil.
46+ // s.base == s <=> s.uri != nil
47+ uri * url.URL
3548 // The schema to which Ref refers.
3649 resolvedRef * Schema
3750
@@ -140,17 +153,16 @@ func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
140153 return nil , err
141154 }
142155
143- m , err := resolveURIs ( s , baseURI )
144- if err != nil {
156+ rs := & Resolved { root : s }
157+ if err := resolveURIs ( rs , baseURI ); err != nil {
145158 return nil , err
146159 }
147- rs := & Resolved {root : s , resolvedURIs : m , resolvedInfo : map [* Schema ]* resolvedInfo {}}
148160
149161 // Remember the schema by both the URI we loaded it from and its canonical name,
150162 // which may differ if the schema has an $id.
151163 // We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
152164 r .loaded [baseURI .String ()] = rs
153- r .loaded [s .uri .String ()] = rs
165+ r .loaded [rs . resolvedInfo [ s ] .uri .String ()] = rs
154166
155167 if err := r .resolveRefs (rs ); err != nil {
156168 return nil , err
@@ -195,10 +207,9 @@ func (root *Schema) checkStructure() error {
195207 }
196208 if s .path != "" {
197209 // We've seen s before.
198- // The schema graph at root is not a tree, but it needs to
199- // be because we assume a unique parent when we store a schema's base
200- // in the Schema. A cycle would also put Schema.all into an infinite
201- // recursion.
210+ // The schema graph at root is not a tree, but it needs to be because
211+ // a schema's base must be unique.
212+ // A cycle would also put Schema.all into an infinite recursion.
202213 return fmt .Errorf ("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)" ,
203214 root , s .path , p )
204215 }
@@ -305,8 +316,6 @@ func (s *Schema) checkLocal(report func(error)) {
305316// to baseURI.
306317// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
307318// 8.2.1.
308-
309- // TODO(jba): dynamicAnchors (§8.2.2)
310319//
311320// Every schema has a base URI and a parent base URI.
312321//
@@ -336,11 +345,17 @@ func (s *Schema) checkLocal(report func(error)) {
336345// allOf/1 http://b.com (absolute $id; doesn't matter that it's not under the loaded URI)
337346// allOf/2 http://a.com/root.json (inherited from parent)
338347// allOf/2/not http://a.com/root.json (inherited from parent)
339- func resolveURIs (root * Schema , baseURI * url.URL ) (map [string ]* Schema , error ) {
340- resolvedURIs := map [string ]* Schema {}
341-
348+ func resolveURIs (rs * Resolved , baseURI * url.URL ) error {
342349 var resolve func (s , base * Schema ) error
343350 resolve = func (s , base * Schema ) error {
351+ assert (rs .resolvedInfo [base ] != nil , "base resolved info not set" )
352+ info := rs .resolvedInfo [s ]
353+ if info == nil {
354+ info = & resolvedInfo {s : s }
355+ rs .resolvedInfo [s ] = info
356+ }
357+ baseURI := rs .resolvedInfo [base ].uri
358+
344359 // ids are scoped to the root.
345360 if s .ID != "" {
346361 // A non-empty ID establishes a new base.
@@ -352,21 +367,21 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
352367 return fmt .Errorf ("$id %s must not have a fragment" , s .ID )
353368 }
354369 // The base URI for this schema is its $id resolved against the parent base.
355- s .uri = base . uri .ResolveReference (idURI )
356- if ! s .uri .IsAbs () {
357- return fmt .Errorf ("$id %s does not resolve to an absolute URI (base is %s)" , s .ID , s . base . uri )
370+ info .uri = baseURI .ResolveReference (idURI )
371+ if ! info .uri .IsAbs () {
372+ return fmt .Errorf ("$id %s does not resolve to an absolute URI (base is %s)" , s .ID , baseURI )
358373 }
359- resolvedURIs [s .uri .String ()] = s
374+ rs . resolvedURIs [info .uri .String ()] = s
360375 base = s // needed for anchors
361376 }
362- s .base = base
377+ info .base = base
363378
364379 // Anchors and dynamic anchors are URI fragments that are scoped to their base.
365380 // We treat them as keys in a map stored within the schema.
366381 setAnchor := func (anchor string , dynamic bool ) error {
367382 if anchor != "" {
368383 if _ , ok := base .anchors [anchor ]; ok {
369- return fmt .Errorf ("duplicate anchor %q in %s" , anchor , base . uri )
384+ return fmt .Errorf ("duplicate anchor %q in %s" , anchor , baseURI )
370385 }
371386 if base .anchors == nil {
372387 base .anchors = map [string ]anchorInfo {}
@@ -388,23 +403,21 @@ func resolveURIs(root *Schema, baseURI *url.URL) (map[string]*Schema, error) {
388403 }
389404
390405 // Set the root URI to the base for now. If the root has an $id, this will change.
391- root .uri = baseURI
392- // The original base, even if changed, is still a valid way to refer to the root.
393- resolvedURIs [baseURI .String ()] = root
394- if err := resolve (root , root ); err != nil {
395- return nil , err
406+ rs .resolvedInfo = map [* Schema ]* resolvedInfo {
407+ rs .root : {s : rs .root , uri : baseURI },
396408 }
397- return resolvedURIs , nil
409+ // The original base, even if changed, is still a valid way to refer to the root.
410+ rs .resolvedURIs = map [string ]* Schema {baseURI .String (): rs .root }
411+
412+ return resolve (rs .root , rs .root )
398413}
399414
400415// resolveRefs replaces every ref in the schemas with the schema it refers to.
401416// A reference that doesn't resolve within the schema may refer to some other schema
402417// that needs to be loaded.
403418func (r * resolver ) resolveRefs (rs * Resolved ) error {
404419 for s := range rs .root .all () {
405- assert (rs .resolvedInfo [s ] == nil , "schema resolved info already set" )
406- info := & resolvedInfo {s : s }
407- rs .resolvedInfo [s ] = info
420+ info := rs .resolvedInfo [s ]
408421 if s .Ref != "" {
409422 refSchema , _ , err := r .resolveRef (rs , s , s .Ref )
410423 if err != nil {
@@ -440,7 +453,8 @@ func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, d
440453 return nil , "" , err
441454 }
442455 // URI-resolve the ref against the current base URI to get a complete URI.
443- refURI = s .base .uri .ResolveReference (refURI )
456+ base := rs .resolvedInfo [s ].base
457+ refURI = rs .resolvedInfo [base ].uri .ResolveReference (refURI )
444458 // The non-fragment part of a ref URI refers to the base URI of some schema.
445459 // This part is the same for dynamic refs too: their non-fragment part resolves
446460 // lexically.
0 commit comments