jsonpointer

package module
v0.23.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 15, 2026 License: Apache-2.0 Imports: 8 Imported by: 151

README

jsonpointer

Tests Coverage CI vuln scan CodeQL

Release Go Report Card CodeFactor Grade License

GoDoc Discord Channel go version Top language Commits since latest release


An implementation of JSON Pointer for golang, which supports go struct.

Announcements

  • 2025-12-19 : new community chat on discord
    • a new discord community channel is available to be notified of changes and support users
    • our venerable Slack channel remains open, and will be eventually discontinued on 2026-03-31

You may join the discord community by clicking the invite link on the discord badge (also above). Discord Channel

Or join our Slack channel: Slack Channelslack-badge

  • 2026-04-15 : added support for trailing "-" for arrays

    • this brings full support of RFC6901
    • this is supported for types relying on the reflection-based implemented
    • API semantics remain essentially unaltered. Exception: Pointer.Set(document any,value any) (document any, err error) can only perform a best-effort to mutate the input document in place. In the case of adding elements to an array with a trailing "-", either pass a mutable array (*[]T) as the input document, or use the returned updated document instead.
    • types that implement the JSONSetable interface may not implement the mutation implied by the trailing "-"
  • 2026-04-15 : added support for optional alternate JSON name providers

    • for struct support the defaults might not suit all situations: there are known limitations when it comes to handle untagged fields or embedded types.
    • the default name provider in use is not fully aligned with go JSON stdlib
    • exposed an option (or global setting) to change the provider that resolves a struct into json keys
    • the default behavior is not altered
    • a new alternate name provider is added (imported from go-openapi/swag/jsonname), aligned with JSON stdlib behavior

Status

API is stable.

Import this library in your project

go get github.com/go-openapi/jsonpointer

Basic usage

See also some examples

Retrieving a value
  import (
    "github.com/go-openapi/jsonpointer"
  )


  var doc any

  ...

	pointer, err := jsonpointer.New("/foo/1")
	if err != nil {
		... // error: e.g. invalid JSON pointer specification
	}

	value, kind, err := pointer.Get(doc)
	if err != nil {
		... // error: e.g. key not found, index out of bounds, etc.
	}

  ...
Setting a value
  ...
  var doc any
  ...
	pointer, err := jsonpointer.New("/foo/1")
	if err != nil {
		... // error: e.g. invalid JSON pointer specification
  }

	doc, err = p.Set(doc, "value")
	if err != nil {
		... // error: e.g. key not found, index out of bounds, etc.
	}

Change log

See https://github.com/go-openapi/jsonpointer/releases

References

https://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07

also known as RFC6901.

Licensing

This library ships under the SPDX-License-Identifier: Apache-2.0.

See the license NOTICE, which recalls the licensing terms of all the pieces of software on top of which it has been built.

Limitations

  • RFC6901 is now fully supported, including trailing "-" semantics for arrays (for Set operations).
  • Default behavior: JSON name detection in go structs
    • Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored.
    • anonymous fields are not traversed if untagged
    • the above limitations may be overcome by calling UseGoNameProvider() at initialization time.
    • alternatively, users may inject the desired custom behavior for naming fields as an option.

Other documentation

Cutting a new release

Maintainers can cut a new release by either:

  • running this workflow
  • or pushing a semver tag
    • signed tags are preferred
    • The tag message is prepended to release notes

Documentation

Overview

Package jsonpointer provides a golang implementation for json pointers.

Example (Iface)
// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"fmt"

	"github.com/go-openapi/jsonpointer"
)

var (
	_ jsonpointer.JSONPointable = CustomDoc{}
	_ jsonpointer.JSONSetable   = &CustomDoc{}
)

// CustomDoc accepts 2 preset properties "propA" and "propB", plus any number of extra properties.
//
// All values are strings.
type CustomDoc struct {
	a string
	b string
	c map[string]string
}

// JSONLookup implements [jsonpointer.JSONPointable].
func (d CustomDoc) JSONLookup(key string) (any, error) {
	switch key {
	case "propA":
		return d.a, nil
	case "propB":
		return d.b, nil
	default:
		if len(d.c) == 0 {
			return nil, fmt.Errorf("key %q not found: %w", key, ErrExampleIface)
		}
		extra, ok := d.c[key]
		if !ok {
			return nil, fmt.Errorf("key %q not found: %w", key, ErrExampleIface)
		}

		return extra, nil
	}
}

// JSONSet implements [jsonpointer.JSONSetable].
func (d *CustomDoc) JSONSet(key string, value any) error {
	asString, ok := value.(string)
	if !ok {
		return fmt.Errorf("a CustomDoc only access strings as values, but got %T: %w", value, ErrExampleIface)
	}

	switch key {
	case "propA":
		d.a = asString

		return nil
	case "propB":
		d.b = asString

		return nil
	default:
		if len(d.c) == 0 {
			d.c = make(map[string]string)
		}
		d.c[key] = asString

		return nil
	}
}

func main() {
	doc := CustomDoc{
		a: "initial value for a",
		b: "initial value for b",
		// no extra values
	}

	pointerA, err := jsonpointer.New("/propA")
	if err != nil {
		fmt.Println(err)

		return
	}

	// get the initial value for a
	propA, kind, err := pointerA.Get(doc)
	if err != nil {
		fmt.Println(err)

		return
	}
	fmt.Printf("propA (%v): %v\n", kind, propA)

	pointerB, err := jsonpointer.New("/propB")
	if err != nil {
		fmt.Println(err)

		return
	}

	// get the initial value for b
	propB, kind, err := pointerB.Get(doc)
	if err != nil {
		fmt.Println(err)

		return
	}
	fmt.Printf("propB (%v): %v\n", kind, propB)

	pointerC, err := jsonpointer.New("/extra")
	if err != nil {
		fmt.Println(err)

		return
	}

	// not found yet
	_, _, err = pointerC.Get(doc)
	fmt.Printf("propC: %v\n", err)

	_, err = pointerA.Set(&doc, "new value for a") // doc is updated in place
	if err != nil {
		fmt.Println(err)

		return
	}

	_, err = pointerB.Set(&doc, "new value for b")
	if err != nil {
		fmt.Println(err)

		return
	}

	_, err = pointerC.Set(&doc, "new extra value")
	if err != nil {
		fmt.Println(err)

		return
	}

	fmt.Printf("updated doc: %v", doc)

}
Output:
propA (string): initial value for a
propB (string): initial value for b
propC: key "extra" not found: example error
updated doc: {new value for a new value for b map[extra:new extra value]}
Example (Struct)
// SPDX-FileCopyrightText: Copyright (c) 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"errors"
	"fmt"

	"github.com/go-openapi/jsonpointer"
)

var ErrExampleIface = errors.New("example error")

type ExampleDoc struct {
	PromotedDoc

	Promoted     EmbeddedDoc `json:"promoted"`
	AnonPromoted EmbeddedDoc `json:"-"`
	A            string      `json:"propA"`
	Ignored      string      `json:"-"`
	Untagged     string

	unexported string
}

type EmbeddedDoc struct {
	B string `json:"propB"`
}

type PromotedDoc struct {
	C string `json:"propC"`
}

func main() {
	doc := ExampleDoc{
		PromotedDoc: PromotedDoc{
			C: "c",
		},
		Promoted: EmbeddedDoc{
			B: "promoted",
		},
		A:          "a",
		Ignored:    "ignored",
		unexported: "unexported",
	}

	{
		// tagged simple field
		pointerA, _ := jsonpointer.New("/propA")
		a, _, err := pointerA.Get(doc)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("a: %v\n", a)
	}

	{
		// tagged struct field is resolved
		pointerB, _ := jsonpointer.New("/promoted/propB")
		b, _, err := pointerB.Get(doc)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("b: %v\n", b)
	}

	{
		// tagged embedded field is resolved
		pointerC, _ := jsonpointer.New("/propC")
		c, _, err := pointerC.Get(doc)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("c: %v\n", c)
	}

	{
		// exlicitly ignored by JSON tag.
		pointerI, _ := jsonpointer.New("/ignored")
		_, _, err := pointerI.Get(doc)
		fmt.Printf("ignored: %v\n", err)
	}

	{
		// unexported field is ignored: use [JSONPointable] to alter this behavior.
		pointerX, _ := jsonpointer.New("/unexported")
		_, _, err := pointerX.Get(doc)
		fmt.Printf("unexported: %v\n", err)
	}

	{
		// Limitation: anonymous field is not resolved.
		pointerC, _ := jsonpointer.New("/propB")
		_, _, err := pointerC.Get(doc)
		fmt.Printf("anonymous: %v\n", err)
	}

	{
		// Limitation: untagged exported field is ignored, unlike with json standard MarshalJSON.
		pointerU, _ := jsonpointer.New("/untagged")
		_, _, err := pointerU.Get(doc)
		fmt.Printf("untagged: %v\n", err)
	}

}
Output:
a: a
b: promoted
c: c
ignored: object has no field "ignored": JSON pointer error
unexported: object has no field "unexported": JSON pointer error
anonymous: object has no field "propB": JSON pointer error
untagged: object has no field "untagged": JSON pointer error

Index

Examples

Constants

View Source
const (
	// ErrPointer is a sentinel error raised by all errors from this package.
	ErrPointer pointerError = "JSON pointer error"

	// ErrInvalidStart states that a JSON pointer must start with a separator ("/").
	ErrInvalidStart pointerError = `JSON pointer must be empty or start with a "` + pointerSeparator + `"`

	// ErrUnsupportedValueType indicates that a value of the wrong type is being set.
	ErrUnsupportedValueType pointerError = "only structs, pointers, maps and slices are supported for setting values"

	// ErrDashToken indicates use of the RFC 6901 "-" reference token
	// in a context where it cannot be resolved.
	//
	// Per RFC 6901 §4 the "-" token refers to the (nonexistent) element
	// after the last array element. It may only be used as the terminal
	// token of a [Pointer.Set] against a slice, where it means "append".
	// Any other use (get, offset, intermediate traversal, non-slice target)
	// is an error condition that wraps this sentinel.
	ErrDashToken pointerError = `the "-" array token cannot be resolved here` //nolint:gosec // G101 false positive: this is a JSON Pointer reference token, not a credential.
)

Variables

This section is empty.

Functions

func Escape

func Escape(token string) string

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 GetForToken

func GetForToken(document any, decodedToken string, opts ...Option) (any, reflect.Kind, error)

GetForToken gets a value for a json pointer token 1 level deep.

func SetDefaultNameProvider added in v0.23.0

func SetDefaultNameProvider(provider NameProvider)

SetDefaultNameProvider sets the NameProvider as a package-level default.

By default, the default provider is jsonname.DefaultJSONNameProvider.

It is safe to call concurrently with Pointer.Get, Pointer.Set, GetForToken and SetForToken. The typical usage is to call it once at initialization time.

A nil provider is ignored.

func SetForToken

func SetForToken(document any, decodedToken string, value any, opts ...Option) (any, error)

SetForToken sets a value for a json pointer token 1 level deep.

See Pointer.Set for the mutation contract, in particular the handling of the RFC 6901 "-" token on slices.

func Unescape

func Unescape(token string) string

Unescape unescapes a json pointer reference token string to the original representation.

func UseGoNameProvider added in v0.23.0

func UseGoNameProvider()

UseGoNameProvider sets the NameProvider as a package-level default to the alternative provider jsonname.GoNameProvider, that covers a few areas not supported by the default name provider.

This implementation supports untagged exported fields and embedded types in go struct. It follows strictly the behavior of the JSON standard library regarding field naming conventions.

It is safe to call concurrently with Pointer.Get, Pointer.Set, GetForToken and SetForToken. The typical usage is to call it once at initialization time.

Example

ExampleUseGoNameProvider contrasts the two NameProvider implementations shipped by github.com/go-openapi/swag/jsonname:

  • the default provider requires a `json` struct tag to expose a field;
  • the Go-name provider follows encoding/json conventions and accepts exported untagged fields and promoted embedded fields as well.
type Embedded struct {
	Nested string // untagged: promoted only by the Go-name provider
}
type Doc struct {
	Embedded // untagged embedded: promoted only by the Go-name provider

	Tagged   string `json:"tagged"`
	Untagged string // no tag: visible only to the Go-name provider
}

doc := Doc{
	Embedded: Embedded{Nested: "promoted"},
	Tagged:   "hit",
	Untagged: "hidden-by-default",
}

for _, path := range []string{"/tagged", "/Untagged", "/Nested"} {
	p, err := New(path)
	if err != nil {
		fmt.Println(err)

		return
	}

	// Default provider: only the tagged field resolves.
	defV, _, defErr := p.Get(doc)
	// Go-name provider: untagged and promoted fields resolve too.
	goV, _, goErr := p.Get(doc, WithNameProvider(jsonname.NewGoNameProvider()))

	fmt.Printf("%s -> default=%v (err=%v) | goname=%v (err=%v)\n",
		path, defV, defErr != nil, goV, goErr != nil)
}
Output:
/tagged -> default=hit (err=false) | goname=hit (err=false)
/Untagged -> default=<nil> (err=true) | goname=hidden-by-default (err=false)
/Nested -> default=<nil> (err=true) | goname=promoted (err=false)

Types

type JSONPointable

type JSONPointable interface {
	// JSONLookup returns a value pointed at this (unescaped) key.
	JSONLookup(key string) (any, error)
}

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 JSONSetable

type JSONSetable interface {
	// JSONSet sets the value pointed at the (unescaped) key.
	//
	// The key may be the RFC 6901 "-" token when the pointer targets a
	// slice-like member; see the interface documentation for details.
	JSONSet(key string, value 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.

Handling of the RFC 6901 "-" token RFC 6901 "-" token" aria-label="Go to Handling of the RFC 6901 "-" token">¶

When a type implementing JSONSetable is the terminal parent of a Pointer.Set call, the library passes the raw reference token to JSONSet without interpretation. In particular, the RFC 6901 "-" token (which conventionally means "append" for arrays, per RFC 6902) is forwarded verbatim as the key argument. Implementations that model an array-like container are expected to give "-" the append semantics; implementations that do not should return an error wrapping ErrDashToken (or ErrPointer) for clarity.

Implementations are responsible for any in-place mutation: the library does not attempt to rebind the result of JSONSet into a parent container.

type NameProvider added in v0.23.0

type NameProvider interface {
	// GetGoName gets the go name for a json property name
	GetGoName(subject any, name string) (string, bool)

	// GetGoNameForType gets the go name for a given type for a json property name
	GetGoNameForType(tpe reflect.Type, name string) (string, bool)
}

NameProvider knows how to resolve go struct fields into json names.

The default provider is brought by github.com/go-openapi/swag/jsonname.DefaultJSONNameProvider.

func DefaultNameProvider added in v0.23.0

func DefaultNameProvider() NameProvider

DefaultNameProvider returns the current package-level NameProvider.

type Option added in v0.23.0

type Option func(*options)

Option to tune the behavior of a JSON Pointer.

func WithNameProvider added in v0.23.0

func WithNameProvider(provider NameProvider) Option

WithNameProvider injects a custom NameProvider to resolve json names from go struct types.

type Pointer

type Pointer struct {
	// contains filtered or unexported fields
}

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

func New

func New(jsonPointerString string) (Pointer, error)

New creates a new json pointer from its string representation.

Example
empty, err := New("")
if err != nil {
	fmt.Println(err)

	return
}
fmt.Printf("empty pointer: %q\n", empty.String())

key, err := New("/foo")
if err != nil {
	fmt.Println(err)

	return
}
fmt.Printf("pointer to object key: %q\n", key.String())

elem, err := New("/foo/1")
if err != nil {
	fmt.Println(err)

	return
}
fmt.Printf("pointer to array element: %q\n", elem.String())

escaped0, err := New("/foo~0")
if err != nil {
	fmt.Println(err)

	return
}
// key contains "~"
fmt.Printf("pointer to key %q: %q\n", Unescape("foo~0"), escaped0.String())

escaped1, err := New("/foo~1")
if err != nil {
	fmt.Println(err)

	return
}
// key contains "/"
fmt.Printf("pointer to key %q: %q\n", Unescape("foo~1"), escaped1.String())
Output:
empty pointer: ""
pointer to object key: "/foo"
pointer to array element: "/foo/1"
pointer to key "foo~": "/foo~0"
pointer to key "foo/": "/foo~1"

func (*Pointer) DecodedTokens

func (p *Pointer) DecodedTokens() []string

DecodedTokens returns the decoded (unescaped) tokens of this JSON pointer.

func (*Pointer) Get

func (p *Pointer) Get(document any, opts ...Option) (any, reflect.Kind, error)

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.

Example
var doc exampleDocument

if err := json.Unmarshal(testDocumentJSONBytes, &doc); err != nil { // populates doc
	fmt.Println(err)

	return
}

pointer, err := New("/foo/1")
if err != nil {
	fmt.Println(err)

	return
}

value, kind, err := pointer.Get(doc)
if err != nil {
	fmt.Println(err)

	return
}

fmt.Printf(
	"value: %q\nkind: %v\n",
	value, kind,
)
Output:
value: "baz"
kind: string

func (*Pointer) IsEmpty

func (p *Pointer) IsEmpty() bool

IsEmpty returns true if this is an empty json pointer.

This indicates that it points to the root document.

func (*Pointer) Offset added in v0.20.0

func (p *Pointer) Offset(document string) (int64, error)

func (*Pointer) Set

func (p *Pointer) Set(document any, value any, opts ...Option) (any, error)

Set uses the pointer to set a value from a data type that represent a JSON document.

Mutation contract

Set mutates the provided document in place whenever Go's type system allows it: when document is a map, a pointer, or when the targeted value is reached through an addressable ancestor (e.g. a struct field traversed via a pointer, a slice element). Callers that rely on this in-place behavior may continue to ignore the returned document.

The returned document is only load-bearing when Set cannot mutate in place. This happens in one specific case: appending to a top-level slice passed by value (e.g. document of type []T rather than *[]T) via the RFC 6901 "-" terminal token. reflect.Append produces a new slice header that the library cannot rebind into the caller's variable; the updated document is returned instead. Pass *[]T if you want in-place rebind for that case as well.

See ErrDashToken for the semantics of the "-" token.

Example
var doc exampleDocument

if err := json.Unmarshal(testDocumentJSONBytes, &doc); err != nil { // populates doc
	fmt.Println(err)

	return
}

pointer, err := New("/foo/1")
if err != nil {
	fmt.Println(err)

	return
}

result, err := pointer.Set(&doc, "hey my")
if err != nil {
	fmt.Println(err)

	return
}

fmt.Printf("result: %#v\n", result)
fmt.Printf("doc: %#v\n", doc)
Output:
result: &jsonpointer.exampleDocument{Foo:[]string{"bar", "hey my"}}
doc: jsonpointer.exampleDocument{Foo:[]string{"bar", "hey my"}}
Example (Append)

ExamplePointer_Set_append demonstrates the RFC 6901 "-" token as an append operation on a slice. On nested slices reached through an addressable parent (map entry, pointer to struct, ...), the append is performed in place and the returned document is the same reference.

doc := map[string]any{"foo": []any{"bar"}}

pointer, err := New("/foo/-")
if err != nil {
	fmt.Println(err)

	return
}

if _, err := pointer.Set(doc, "baz"); err != nil {
	fmt.Println(err)

	return
}

fmt.Printf("doc: %v\n", doc["foo"])
Output:
doc: [bar baz]
Example (AppendTopLevelSlice)

ExamplePointer_Set_appendTopLevelSlice shows the one case where the returned document is load-bearing: appending to a top-level slice passed by value. The library cannot rebind the slice header in the caller's variable, so callers must use the returned document (or pass *[]T to get in-place rebind).

doc := []int{1, 2}

pointer, err := New("/-")
if err != nil {
	fmt.Println(err)

	return
}

out, err := pointer.Set(doc, 3)
if err != nil {
	fmt.Println(err)

	return
}

fmt.Printf("original: %v\n", doc)
fmt.Printf("returned: %v\n", out)
Output:
original: [1 2]
returned: [1 2 3]

func (*Pointer) String

func (p *Pointer) String() string

String representation of a pointer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL