// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 // Package jsonpointer provides a golang implementation for json pointers. package jsonpointer import ( "encoding/json" "errors" "fmt" "reflect" "strconv" "strings" "github.com/go-openapi/swag/jsonname" ) const ( emptyPointer = `` pointerSeparator = `/` ) // JSONPointable is an interface for structs to implement, // when they need to customize the json pointer process or want to avoid the use of reflection. type JSONPointable interface { // JSONLookup returns a value pointed at this (unescaped) key. JSONLookup(key string) (any, error) } // JSONSetable is an interface for structs to implement, // when they need to customize the json pointer process or want to avoid the use of reflection. type JSONSetable interface { // JSONSet sets the value pointed at the (unescaped) key. JSONSet(key string, value any) error } // Pointer is a representation of a json pointer. // // Use [Pointer.Get] to retrieve a value or [Pointer.Set] to set a value. // // It works with any go type interpreted as a JSON document, which means: // // - if a type implements [JSONPointable], its [JSONPointable.JSONLookup] method is used to resolve [Pointer.Get] // - if a type implements [JSONSetable], its [JSONPointable.JSONSet] method is used to resolve [Pointer.Set] // - a go map[K]V is interpreted as an object, with type K assignable to a string // - a go slice []T is interpreted as an array // - a go struct is interpreted as an object, with exported fields interpreted as keys // - promoted fields from an embedded struct are traversed // - scalars (e.g. int, float64 ...), channels, functions and go arrays cannot be traversed // // For struct s resolved by reflection, key mappings honor the conventional struct tag `json`. // // Fields that do not specify a `json` tag, or specify an empty one, or are tagged as `json:"-"` are ignored. // // # Limitations // // - Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored. // - anonymous fields are not traversed if untagged type Pointer struct { referenceTokens []string } // New creates a new json pointer from its string representation. func New(jsonPointerString string) (Pointer, error) { var p Pointer err := p.parse(jsonPointerString) return p, err } // Get uses the pointer to retrieve a value from a JSON document. // // It returns the value with its type as a [reflect.Kind] or an error. func (p *Pointer) Get(document any) (any, reflect.Kind, error) { return p.get(document, jsonname.DefaultJSONNameProvider) } // Set uses the pointer to set a value from a data type // that represent a JSON document. // // It returns the updated document. func (p *Pointer) Set(document any, value any) (any, error) { return document, p.set(document, value, jsonname.DefaultJSONNameProvider) } // DecodedTokens returns the decoded (unescaped) tokens of this JSON pointer. func (p *Pointer) DecodedTokens() []string { result := make([]string, 0, len(p.referenceTokens)) for _, token := range p.referenceTokens { result = append(result, Unescape(token)) } return result } // IsEmpty returns true if this is an empty json pointer. // // This indicates that it points to the root document. func (p *Pointer) IsEmpty() bool { return len(p.referenceTokens) == 0 } // String representation of a pointer. func (p *Pointer) String() string { if len(p.referenceTokens) == 0 { return emptyPointer } return pointerSeparator + strings.Join(p.referenceTokens, pointerSeparator) } func (p *Pointer) Offset(document string) (int64, error) { dec := json.NewDecoder(strings.NewReader(document)) var offset int64 for _, ttk := range p.DecodedTokens() { tk, err := dec.Token() if err != nil { return 0, err } switch tk := tk.(type) { case json.Delim: switch tk { case '{': offset, err = offsetSingleObject(dec, ttk) if err != nil { return 0, err } case '[': offset, err = offsetSingleArray(dec, ttk) if err != nil { return 0, err } default: return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer) } default: return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer) } } return offset, nil } // "Constructor", parses the given string JSON pointer. func (p *Pointer) parse(jsonPointerString string) error { if jsonPointerString == emptyPointer { return nil } if !strings.HasPrefix(jsonPointerString, pointerSeparator) { // non empty pointer must start with "/" return errors.Join(ErrInvalidStart, ErrPointer) } referenceTokens := strings.Split(jsonPointerString, pointerSeparator) p.referenceTokens = append(p.referenceTokens, referenceTokens[1:]...) return nil } func (p *Pointer) get(node any, nameProvider *jsonname.NameProvider) (any, reflect.Kind, error) { if nameProvider == nil { nameProvider = jsonname.DefaultJSONNameProvider } kind := reflect.Invalid // full document when empty if len(p.referenceTokens) == 0 { return node, kind, nil } for _, token := range p.referenceTokens { decodedToken := Unescape(token) r, knd, err := getSingleImpl(node, decodedToken, nameProvider) if err != nil { return nil, knd, err } node = r } rValue := reflect.ValueOf(node) kind = rValue.Kind() return node, kind, nil } func (p *Pointer) set(node, data any, nameProvider *jsonname.NameProvider) error { knd := reflect.ValueOf(node).Kind() if knd != reflect.Pointer && knd != reflect.Struct && knd != reflect.Map && knd != reflect.Slice && knd != reflect.Array { return errors.Join( fmt.Errorf("unexpected type: %T", node), //nolint:err113 // err wrapping is carried out by errors.Join, not fmt.Errorf. ErrUnsupportedValueType, ErrPointer, ) } l := len(p.referenceTokens) // full document when empty if l == 0 { return nil } if nameProvider == nil { nameProvider = jsonname.DefaultJSONNameProvider } var decodedToken string lastIndex := l - 1 if lastIndex > 0 { // skip if we only have one token in pointer for _, token := range p.referenceTokens[:lastIndex] { decodedToken = Unescape(token) next, err := p.resolveNodeForToken(node, decodedToken, nameProvider) if err != nil { return err } node = next } } // last token decodedToken = Unescape(p.referenceTokens[lastIndex]) return setSingleImpl(node, data, decodedToken, nameProvider) } func (p *Pointer) resolveNodeForToken(node any, decodedToken string, nameProvider *jsonname.NameProvider) (next any, err error) { // check for nil during traversal if isNil(node) { return nil, fmt.Errorf("cannot traverse through nil value at %q: %w", decodedToken, ErrPointer) } pointable, ok := node.(JSONPointable) if ok { r, err := pointable.JSONLookup(decodedToken) if err != nil { return nil, err } fld := reflect.ValueOf(r) if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Pointer { return fld.Addr().Interface(), nil } return r, nil } rValue := reflect.Indirect(reflect.ValueOf(node)) kind := rValue.Kind() switch kind { case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return nil, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) } return typeFromValue(rValue.FieldByName(nm)), nil case reflect.Map: kv := reflect.ValueOf(decodedToken) mv := rValue.MapIndex(kv) if !mv.IsValid() { return nil, errNoKey(decodedToken) } return typeFromValue(mv), nil case reflect.Slice: tokenIndex, err := strconv.Atoi(decodedToken) if err != nil { return nil, errors.Join(err, ErrPointer) } sLength := rValue.Len() if tokenIndex < 0 || tokenIndex >= sLength { return nil, errOutOfBounds(sLength, tokenIndex) } return typeFromValue(rValue.Index(tokenIndex)), nil default: return nil, errInvalidReference(decodedToken) } } func isNil(input any) bool { if input == nil { return true } kind := reflect.TypeOf(input).Kind() switch kind { case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Chan: return reflect.ValueOf(input).IsNil() default: return false } } func typeFromValue(v reflect.Value) any { if v.CanAddr() && v.Kind() != reflect.Interface && v.Kind() != reflect.Map && v.Kind() != reflect.Slice && v.Kind() != reflect.Pointer { return v.Addr().Interface() } return v.Interface() } // GetForToken gets a value for a json pointer token 1 level deep. func GetForToken(document any, decodedToken string) (any, reflect.Kind, error) { return getSingleImpl(document, decodedToken, jsonname.DefaultJSONNameProvider) } // SetForToken sets a value for a json pointer token 1 level deep. func SetForToken(document any, decodedToken string, value any) (any, error) { return document, setSingleImpl(document, value, decodedToken, jsonname.DefaultJSONNameProvider) } func getSingleImpl(node any, decodedToken string, nameProvider *jsonname.NameProvider) (any, reflect.Kind, error) { rValue := reflect.Indirect(reflect.ValueOf(node)) kind := rValue.Kind() if isNil(node) { return nil, kind, fmt.Errorf("nil value has no field %q: %w", decodedToken, ErrPointer) } switch typed := node.(type) { case JSONPointable: r, err := typed.JSONLookup(decodedToken) if err != nil { return nil, kind, err } return r, kind, nil case *any: // case of a pointer to interface, that is not resolved by reflect.Indirect return getSingleImpl(*typed, decodedToken, nameProvider) } switch kind { case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return nil, kind, fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) } fld := rValue.FieldByName(nm) return fld.Interface(), kind, nil case reflect.Map: kv := reflect.ValueOf(decodedToken) mv := rValue.MapIndex(kv) if mv.IsValid() { return mv.Interface(), kind, nil } return nil, kind, errNoKey(decodedToken) case reflect.Slice: tokenIndex, err := strconv.Atoi(decodedToken) if err != nil { return nil, kind, errors.Join(err, ErrPointer) } sLength := rValue.Len() if tokenIndex < 0 || tokenIndex >= sLength { return nil, kind, errOutOfBounds(sLength, tokenIndex) } elem := rValue.Index(tokenIndex) return elem.Interface(), kind, nil default: return nil, kind, errInvalidReference(decodedToken) } } func setSingleImpl(node, data any, decodedToken string, nameProvider *jsonname.NameProvider) error { // check for nil to prevent panic when calling rValue.Type() if isNil(node) { return fmt.Errorf("cannot set field %q on nil value: %w", decodedToken, ErrPointer) } if ns, ok := node.(JSONSetable); ok { return ns.JSONSet(decodedToken, data) } rValue := reflect.Indirect(reflect.ValueOf(node)) switch rValue.Kind() { case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { return fmt.Errorf("object has no field %q: %w", decodedToken, ErrPointer) } fld := rValue.FieldByName(nm) if !fld.CanSet() { return fmt.Errorf("can't set struct field %s to %v: %w", nm, data, ErrPointer) } value := reflect.ValueOf(data) valueType := value.Type() assignedType := fld.Type() if !valueType.AssignableTo(assignedType) { return fmt.Errorf("can't set value with type %T to field %s with type %v: %w", data, nm, assignedType, ErrPointer) } fld.Set(value) return nil case reflect.Map: kv := reflect.ValueOf(decodedToken) rValue.SetMapIndex(kv, reflect.ValueOf(data)) return nil case reflect.Slice: tokenIndex, err := strconv.Atoi(decodedToken) if err != nil { return errors.Join(err, ErrPointer) } sLength := rValue.Len() if tokenIndex < 0 || tokenIndex >= sLength { return errOutOfBounds(sLength, tokenIndex) } elem := rValue.Index(tokenIndex) if !elem.CanSet() { return fmt.Errorf("can't set slice index %s to %v: %w", decodedToken, data, ErrPointer) } value := reflect.ValueOf(data) valueType := value.Type() assignedType := elem.Type() if !valueType.AssignableTo(assignedType) { return fmt.Errorf("can't set value with type %T to slice element %d with type %v: %w", data, tokenIndex, assignedType, ErrPointer) } elem.Set(value) return nil default: return errInvalidReference(decodedToken) } } func offsetSingleObject(dec *json.Decoder, decodedToken string) (int64, error) { for dec.More() { offset := dec.InputOffset() tk, err := dec.Token() if err != nil { return 0, err } switch tk := tk.(type) { case json.Delim: switch tk { case '{': if err = drainSingle(dec); err != nil { return 0, err } case '[': if err = drainSingle(dec); err != nil { return 0, err } } case string: if tk == decodedToken { return offset, nil } default: return 0, fmt.Errorf("invalid token %#v: %w", tk, ErrPointer) } } return 0, fmt.Errorf("token reference %q not found: %w", decodedToken, ErrPointer) } func offsetSingleArray(dec *json.Decoder, decodedToken string) (int64, error) { idx, err := strconv.Atoi(decodedToken) if err != nil { return 0, fmt.Errorf("token reference %q is not a number: %w: %w", decodedToken, err, ErrPointer) } var i int for i = 0; i < idx && dec.More(); i++ { tk, err := dec.Token() if err != nil { return 0, err } if delim, isDelim := tk.(json.Delim); isDelim { switch delim { case '{': if err = drainSingle(dec); err != nil { return 0, err } case '[': if err = drainSingle(dec); err != nil { return 0, err } } } } if !dec.More() { return 0, fmt.Errorf("token reference %q not found: %w", decodedToken, ErrPointer) } return dec.InputOffset(), nil } // drainSingle drains a single level of object or array. // // The decoder has to guarantee the beginning delim (i.e. '{' or '[') has been consumed. func drainSingle(dec *json.Decoder) error { for dec.More() { tk, err := dec.Token() if err != nil { return err } if delim, isDelim := tk.(json.Delim); isDelim { switch delim { case '{': if err = drainSingle(dec); err != nil { return err } case '[': if err = drainSingle(dec); err != nil { return err } } } } // consumes the ending delim if _, err := dec.Token(); err != nil { return err } return nil } // JSON pointer encoding: // ~0 => ~ // ~1 => / // ... and vice versa const ( encRefTok0 = `~0` encRefTok1 = `~1` decRefTok0 = `~` decRefTok1 = `/` ) var ( encRefTokReplacer = strings.NewReplacer(encRefTok1, decRefTok1, encRefTok0, decRefTok0) //nolint:gochecknoglobals // it's okay to declare a replacer as a private global decRefTokReplacer = strings.NewReplacer(decRefTok1, encRefTok1, decRefTok0, encRefTok0) //nolint:gochecknoglobals // it's okay to declare a replacer as a private global ) // Unescape unescapes a json pointer reference token string to the original representation. func Unescape(token string) string { return encRefTokReplacer.Replace(token) } // Escape escapes a pointer reference token string. // // The JSONPointer specification defines "/" as a separator and "~" as an escape prefix. // // Keys containing such characters are escaped with the following rules: // // - "~" is escaped as "~0" // - "/" is escaped as "~1" func Escape(token string) string { return decRefTokReplacer.Replace(token) }