Skip to content

Commit 18d8150

Browse files
committed
move all unexported Schema fields
1 parent 53bc86d commit 18d8150

File tree

4 files changed

+102
-82
lines changed

4 files changed

+102
-82
lines changed

jsonschema/resolve.go

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ type Resolved struct {
2929
resolvedInfo map[*Schema]*resolvedInfo
3030
}
3131

32+
func newResolved(s *Schema) *Resolved {
33+
return &Resolved{
34+
root: s,
35+
resolvedURIs: map[string]*Schema{},
36+
resolvedInfo: map[*Schema]*resolvedInfo{},
37+
}
38+
}
39+
3240
// resolvedInfo holds information specific to a schema that is computed by [Schema.Resolve].
3341
type resolvedInfo struct {
3442
s *Schema
@@ -57,12 +65,37 @@ type resolvedInfo struct {
5765
resolvedDynamicRef *Schema
5866
// The anchor to look up on the stack when the dynamic ref acts dynamically.
5967
dynamicRefAnchor string
68+
69+
// The following fields are independent of arguments to Schema.Resolved,
70+
// so they could live on the Schema. We put them here for simplicity.
71+
72+
// The set of required properties.
73+
isRequired map[string]bool
74+
75+
// Compiled regexps.
76+
pattern *regexp.Regexp
77+
patternProperties map[*regexp.Regexp]*Schema
78+
79+
// Map from anchors to subschemas.
80+
anchors map[string]anchorInfo
6081
}
6182

6283
// Schema returns the schema that was resolved.
6384
// It must not be modified.
6485
func (r *Resolved) Schema() *Schema { return r.root }
6586

87+
// schemaString returns a short string describing the schema.
88+
func (r *Resolved) schemaString(s *Schema) string {
89+
if s.ID != "" {
90+
return s.ID
91+
}
92+
info := r.resolvedInfo[s]
93+
if info.path != "" {
94+
return info.path
95+
}
96+
return "<anonymous schema>"
97+
}
98+
6699
// A Loader reads and unmarshals the schema at uri, if any.
67100
type Loader func(uri *url.URL) (*Schema, error)
68101

@@ -152,12 +185,9 @@ func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
152185
if baseURI.Fragment != "" {
153186
return nil, fmt.Errorf("base URI %s must not have a fragment", baseURI)
154187
}
155-
rs := &Resolved{root: s, resolvedInfo:map[*Schema]*resolvedInfo{}}
156-
for s := rs.root.all() {
157-
rs.resolvedInfo[s] = &resolvedInfo{s:s}
158-
}
188+
rs := newResolved(s)
159189

160-
if err := s.check(); err != nil {
190+
if err := s.check(rs.resolvedInfo); err != nil {
161191
return nil, err
162192
}
163193

@@ -177,29 +207,27 @@ func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
177207
return rs, nil
178208
}
179209

180-
func (root *Schema) check() error {
210+
func (root *Schema) check(infos map[*Schema]*resolvedInfo) error {
181211
// Check for structural validity. Do this first and fail fast:
182212
// bad structure will cause other code to panic.
183-
if err := root.checkStructure(); err != nil {
213+
if err := root.checkStructure(infos); err != nil {
184214
return err
185215
}
186216

187217
var errs []error
188218
report := func(err error) { errs = append(errs, err) }
189219

190220
for ss := range root.all() {
191-
ss.checkLocal(report)
221+
ss.checkLocal(report, infos)
192222
}
193223
return errors.Join(errs...)
194224
}
195225

196226
// checkStructure verifies that root and its subschemas form a tree.
197227
// It also assigns each schema a unique path, to improve error messages.
198-
func (root *Schema) checkStructure() error {
199-
if root.path != "" {
200-
// We have done this before, and it will always produce the same result.
201-
return nil
202-
}
228+
func (root *Schema) checkStructure(infos map[*Schema]*resolvedInfo) error {
229+
assert(len(infos) == 0, "non-empty infos")
230+
203231
var check func(reflect.Value, []byte) error
204232
check = func(v reflect.Value, path []byte) error {
205233
// For the purpose of error messages, the root schema has path "root"
@@ -212,16 +240,15 @@ func (root *Schema) checkStructure() error {
212240
if s == nil {
213241
return fmt.Errorf("jsonschema: schema at %s is nil", p)
214242
}
215-
if s.path != "" {
243+
if info, ok := infos[s]; ok {
216244
// We've seen s before.
217245
// The schema graph at root is not a tree, but it needs to
218246
// be because a schema's base must be unique.
219-
// A cycle would also put Schema.all into an infinite
220-
// recursion.
247+
// A cycle would also put Schema.all into an infinite recursion.
221248
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
222-
root, s.path, p)
249+
root, info.path, p)
223250
}
224-
s.path = p
251+
infos[s] = &resolvedInfo{s: s, path: p}
225252

226253
for _, info := range schemaFieldInfos {
227254
fv := v.Elem().FieldByIndex(info.sf.Index)
@@ -263,7 +290,7 @@ func (root *Schema) checkStructure() error {
263290
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
264291
// in the schema for later use.
265292
// It appends the errors it finds to errs.
266-
func (s *Schema) checkLocal(report func(error)) {
293+
func (s *Schema) checkLocal(report func(error), infos map[*Schema]*resolvedInfo) {
267294
addf := func(format string, args ...any) {
268295
msg := fmt.Sprintf(format, args...)
269296
report(fmt.Errorf("jsonschema.Schema: %s: %s", s, msg))
@@ -289,33 +316,35 @@ func (s *Schema) checkLocal(report func(error)) {
289316
addf("cannot validate a schema with $vocabulary")
290317
}
291318

319+
info := infos[s]
320+
292321
// Check and compile regexps.
293322
if s.Pattern != "" {
294323
re, err := regexp.Compile(s.Pattern)
295324
if err != nil {
296325
addf("pattern: %v", err)
297326
} else {
298-
s.pattern = re
327+
info.pattern = re
299328
}
300329
}
301330
if len(s.PatternProperties) > 0 {
302-
s.patternProperties = map[*regexp.Regexp]*Schema{}
331+
info.patternProperties = map[*regexp.Regexp]*Schema{}
303332
for reString, subschema := range s.PatternProperties {
304333
re, err := regexp.Compile(reString)
305334
if err != nil {
306335
addf("patternProperties[%q]: %v", reString, err)
307336
continue
308337
}
309-
s.patternProperties[re] = subschema
338+
info.patternProperties[re] = subschema
310339
}
311340
}
312341

313342
// Build a set of required properties, to avoid quadratic behavior when validating
314343
// a struct.
315344
if len(s.Required) > 0 {
316-
s.isRequired = map[string]bool{}
345+
info.isRequired = map[string]bool{}
317346
for _, r := range s.Required {
318-
s.isRequired[r] = true
347+
info.isRequired[r] = true
319348
}
320349
}
321350
}
@@ -356,13 +385,8 @@ func (s *Schema) checkLocal(report func(error)) {
356385
func resolveURIs(rs *Resolved, baseURI *url.URL) error {
357386
var resolve func(s, base *Schema) error
358387
resolve = func(s, base *Schema) error {
359-
assert(rs.resolvedInfo[base] != nil, "base resolved info not set")
360388
info := rs.resolvedInfo[s]
361-
if info == nil {
362-
info = &resolvedInfo{s: s}
363-
rs.resolvedInfo[s] = info
364-
}
365-
baseURI := rs.resolvedInfo[base].uri
389+
baseInfo := rs.resolvedInfo[base]
366390

367391
// ids are scoped to the root.
368392
if s.ID != "" {
@@ -375,26 +399,27 @@ func resolveURIs(rs *Resolved, baseURI *url.URL) error {
375399
return fmt.Errorf("$id %s must not have a fragment", s.ID)
376400
}
377401
// The base URI for this schema is its $id resolved against the parent base.
378-
info.uri = baseURI.ResolveReference(idURI)
402+
info.uri = baseInfo.uri.ResolveReference(idURI)
379403
if !info.uri.IsAbs() {
380-
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %s)", s.ID, baseURI)
404+
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %q)", s.ID, baseInfo.uri)
381405
}
382406
rs.resolvedURIs[info.uri.String()] = s
383407
base = s // needed for anchors
408+
baseInfo = rs.resolvedInfo[base]
384409
}
385410
info.base = base
386411

387412
// Anchors and dynamic anchors are URI fragments that are scoped to their base.
388413
// We treat them as keys in a map stored within the schema.
389414
setAnchor := func(anchor string, dynamic bool) error {
390415
if anchor != "" {
391-
if _, ok := base.anchors[anchor]; ok {
392-
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseURI)
416+
if _, ok := baseInfo.anchors[anchor]; ok {
417+
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseInfo.uri)
393418
}
394-
if base.anchors == nil {
395-
base.anchors = map[string]anchorInfo{}
419+
if baseInfo.anchors == nil {
420+
baseInfo.anchors = map[string]anchorInfo{}
396421
}
397-
base.anchors[anchor] = anchorInfo{s, dynamic}
422+
baseInfo.anchors[anchor] = anchorInfo{s, dynamic}
398423
}
399424
return nil
400425
}
@@ -411,11 +436,9 @@ func resolveURIs(rs *Resolved, baseURI *url.URL) error {
411436
}
412437

413438
// Set the root URI to the base for now. If the root has an $id, this will change.
414-
rs.resolvedInfo = map[*Schema]*resolvedInfo{
415-
rs.root: {s: rs.root, uri: baseURI},
416-
}
439+
rs.resolvedInfo[rs.root].uri = baseURI
417440
// The original base, even if changed, is still a valid way to refer to the root.
418-
rs.resolvedURIs = map[string]*Schema{baseURI.String(): rs.root}
441+
rs.resolvedURIs[baseURI.String()] = rs.root
419442

420443
return resolve(rs.root, rs.root)
421444
}
@@ -508,7 +531,9 @@ func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, d
508531
// A JSON Pointer is either the empty string or begins with a '/',
509532
// whereas anchors are always non-empty strings that don't contain slashes.
510533
if frag != "" && !strings.HasPrefix(frag, "/") {
511-
info, found := referencedSchema.anchors[frag]
534+
resInfo := rs.resolvedInfo[referencedSchema]
535+
info, found := resInfo.anchors[frag]
536+
512537
if !found {
513538
return nil, "", fmt.Errorf("no anchor %q in %s", frag, s)
514539
}

jsonschema/resolve_test.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import (
1717
func TestSchemaStructure(t *testing.T) {
1818
check := func(s *Schema, want string) {
1919
t.Helper()
20-
err := s.checkStructure()
20+
infos := map[*Schema]*resolvedInfo{}
21+
err := s.checkStructure(infos)
2122
if err == nil || !strings.Contains(err.Error(), want) {
2223
t.Errorf("checkStructure returned error %q, want %q", err, want)
2324
}
@@ -89,13 +90,14 @@ func TestPaths(t *testing.T) {
8990
{root.PrefixItems[1], "/prefixItems/1"},
9091
{root.PrefixItems[1].Items, "/prefixItems/1/items"},
9192
}
92-
if err := root.checkStructure(); err != nil {
93+
rs := newResolved(root)
94+
if err := root.checkStructure(rs.resolvedInfo); err != nil {
9395
t.Fatal(err)
9496
}
9597

9698
var got []item
9799
for s := range root.all() {
98-
got = append(got, item{s, s.path})
100+
got = append(got, item{s, rs.resolvedInfo[s].path})
99101
}
100102
if !slices.Equal(got, want) {
101103
t.Errorf("\ngot %v\nwant %v", got, want)
@@ -130,7 +132,10 @@ func TestResolveURIs(t *testing.T) {
130132
t.Fatal(err)
131133
}
132134

133-
rs := &Resolved{root: root}
135+
rs := newResolved(root)
136+
if err := root.check(rs.resolvedInfo); err != nil {
137+
t.Fatal(err)
138+
}
134139
if err := resolveURIs(rs, base); err != nil {
135140
t.Fatal(err)
136141
}
@@ -165,11 +170,12 @@ func TestResolveURIs(t *testing.T) {
165170
t.Errorf("IDs:\ngot %+v\n\nwant %+v", got, wantIDs)
166171
}
167172
for s := range root.all() {
173+
info := rs.resolvedInfo[s]
168174
if want := wantAnchors[s]; want != nil {
169-
if got := s.anchors; !maps.Equal(got, want) {
175+
if got := info.anchors; !maps.Equal(got, want) {
170176
t.Errorf("anchors:\ngot %+v\n\nwant %+v", got, want)
171177
}
172-
} else if s.anchors != nil {
178+
} else if info.anchors != nil {
173179
t.Errorf("non-nil anchors for %s", s)
174180
}
175181
}

jsonschema/schema.go

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"maps"
1515
"math"
1616
"reflect"
17-
"regexp"
1817
"slices"
1918

2019
"github.com/modelcontextprotocol/go-sdk/internal/util"
@@ -128,19 +127,6 @@ type Schema struct {
128127

129128
// Extra allows for additional keywords beyond those specified.
130129
Extra map[string]any `json:"-"`
131-
132-
// These fields are independent of arguments to Schema.Resolved,
133-
// though they are computed there.
134-
135-
// Map from anchors to subschemas.
136-
anchors map[string]anchorInfo
137-
138-
// compiled regexps
139-
pattern *regexp.Regexp
140-
patternProperties map[*regexp.Regexp]*Schema
141-
142-
// the set of required properties
143-
isRequired map[string]bool
144130
}
145131

146132
// falseSchema returns a new Schema tree that fails to validate any value.
@@ -157,12 +143,12 @@ type anchorInfo struct {
157143

158144
// String returns a short description of the schema.
159145
func (s *Schema) String() string {
146+
if s.ID != "" {
147+
return s.ID
148+
}
160149
if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" {
161150
return fmt.Sprintf("anchor %s", a)
162151
}
163-
if s.path != "" {
164-
return s.path
165-
}
166152
return "<anonymous schema>"
167153
}
168154

0 commit comments

Comments
 (0)