// 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 sf, ok := schemaFieldMap[name]; ok { return v.FieldByIndex(sf.Index) } return reflect.Value{} }