jp

package module
v0.9.6 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2025 License: MIT Imports: 12 Imported by: 5

README

jp is a byte buffer oriented library for parsing and manipulating JSON.

jp is a fork of jsonparser. It keeps the fast zero-allocation method for accessing individual values in a JSON document, and adds a few new features:

  • Index allows you to parse a buffer full of JSON once and then read and manipulate it in various ways with minimal additional CPU and memory overhead.

  • Patch allows you to create, manipulate, and apply JSON patches without needing excess remarshalling or memory allocation.

Rationale

The original jsonparser library is great for retrieving arbitrary values from JSON documents, but when you have to do a lot of it to the same (potentially large) document or make a lot of modifications to the document (which may be interspersed with additional reads), the overhead of reparsing the JSON can start to outweigh the benefits of having zero allocations.

Otherwise, it maintains the original jsonparser virtues of being easy to use, low overhead, and high performance when you don't want to marshal to and from a structure or a map.

Example

For the given JSON our goal is to extract the user's full name, number of github followers and avatar.

import "gitlab.com/rackn/jp"

...

data := []byte(`{
  "person": {
    "name": {
      "first": "Leonid",
      "last": "Bugaev",
      "fullName": "Leonid Bugaev"
    },
    "github": {
      "handle": "buger",
      "followers": 109
    },
    "avatars": [
      { "url": "https://avatars1.githubusercontent.com/u/14009?v=3&s=460", "type": "thumbnail" }
    ]
  },
  "company": {
    "name": "Acme"
  }
}`)

// You can specify key path by providing arguments to Get function
jp.Get(data, "person", "name", "fullName")

// There is `GetInt` and `GetBoolean` helpers if you exactly know key data type
jp.GetInt(data, "person", "github", "followers")

// When you try to get object, it will return you []byte slice pointer to data containing it
// In `company` it will be `{"name": "Acme"}`
jp.Get(data, "company")

// If the key doesn't exist it will throw an error
var size int64
if value, err := jp.GetInt(data, "company", "size"); err == nil {
  size = value
}

// You can use `ArrayEach` helper to iterate items [item1, item2 .... itemN]
jp.ArrayEach(data, func(value []byte, dataType jp.ValueType, offset int, err error) {
	fmt.Println(jp.Get(value, "url"))
}, "person", "avatars")

// Or use can access fields by index!
jp.GetString(data, "person", "avatars", "[0]", "url")

// Non-braced numeric indexes work too
jp.GetString(data, "person", "avatars", "0", "url")

// So do negative indexes -- they count from the end of the array, but incur additional overhead.
jp.GetString(data, "person", "avatars", "-1", "url")

// You can use `ObjectEach` helper to iterate objects { "key1":object1, "key2":object2, .... "keyN":objectN }
jp.ObjectEach(data, func(key []byte, value []byte, dataType jp.ValueType, offset int) error {
        fmt.Printf("Key: '%s'\n Value: '%s'\n Type: %s\n", string(key), string(value), dataType)
	return nil
}, "person", "name")

// If you want to fold, spindle, or mutilate your JSON, make an Index out of it:
idx, err := jp.MakeIndex(data)

// Now, you can make whatever changes you want in a lazy fashion:
idx.Delete("person","github")
idx.Add(`{"handle":"rackn","followers":0}`,"person","gitlab")

// And then create a new document with the changes:
data,_ = index.MarshalJSON()
// For more information see docs below

Reference

You can view the API on pkg.go.dev.

On keys ...string parameters

You will see that most of the functions take a parameter named keys as their final variable, with a type signature of ...string. All these functions can descend to arbitrary locations in a set of nested JSON objects and arrays in any combination.

When one of the functions is descending into a JSON object, it interprets whatever member of keys (the key) it is considering as a string that should match an object key.

When descending into an Array, the key must be in one of the following forms:

  • A positive or negative integer. Values from 0 up refer to array indexes from the beginning of an array, and values from -1 down refer to indexes from the end of the array.
  • As above but surrounded by []. This is the format that jsonparser supported, presumably for disambiguation.
  • [-] or - when calling index.Add. This allows callers to append a value at the end of an array without knowing its length beforehand.

What makes it so fast?

  • All of the core functions do not allocate and only parse through byte buffers. The values they return are slices of the byte buffer they were passed
  • Creating Index values using IndexValue is single-pass and only parses enough to create a full Index of a single JSON object. It allocates the minimum amount of memory necessary to hold the key indexes, and initially maps all values directly to the buffer it was originally created from.
  • Modifying values using Index methods do not modify the backing byte array.
  • No automatic type conversions, by default everything is a []byte, but it provides you value type, so you can convert by yourself using provided helpers.
  • Does not parse full record, only keys you specified when using the core functions.

Documentation

Index

Constants

View Source
const (
	NotExist = ValueType(iota)
	String
	Number
	Object
	Array
	Boolean
	Null
	Unknown = ValueType(255)
)
View Source
const ContentType = "application/json-patch+json"

Variables

View Source
var (
	StopIteration              = errors.New("stop iteration")
	KeyPathNotFoundError       = errors.New("Key path not found")
	KeyPathFoundError          = errors.New("Key path exists")
	UnknownValueTypeError      = errors.New("Unknown value type")
	MalformedJsonError         = errors.New("Malformed JSON error")
	MalformedStringError       = errors.New("Value is string, but can't find closing '\"' symbol")
	MalformedArrayError        = errors.New("Value is array, but can't find closing ']' symbol")
	MalformedObjectError       = errors.New("Value looks like object, but can't find closing '}' symbol")
	MalformedValueError        = errors.New("Value looks like Number/Boolean/None, but can't find its end: ',' or '}' symbol")
	OverflowIntegerError       = errors.New("Value is number, but overflowed while parsing")
	MalformedNumberError       = errors.New("Value is number, but starts '0' or '-0' or is an invalid float")
	MalformedStringEscapeError = errors.New("Encountered an invalid escape sequence in a string")
	NotStringError             = errors.New("Value is not a string")
	NotFloatError              = errors.New("Value is not a float")
	NotIntegerError            = errors.New("Value is not an integer")
	NotBooleanError            = errors.New("Value is not a boolean")
	NotNullError               = errors.New("Value is not null")
	NotEqualError              = errors.New("Values are not equal")
	BadMoveError               = errors.New("Cannot move a value into itself")
	BadPatchOpError            = errors.New("Invalid patch Op")
	NotArrayError              = errors.New("Value is not an array")
	NotObjectError             = errors.New("Value is not an object")
)

Errors

View Source
var (
	ErrMissingOp   = errors.New("missing op")
	ErrMissingPath = errors.New("missing path")
)
View Source
var IllegalPointerError = fmt.Errorf("illegal pointer")

Functions

func ArrayEach

func ArrayEach(data []byte, cb func(value []byte, dataType ValueType, offset int, err error) error, keys ...string) error

ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`.

func Equal

func Equal(a, b []byte) bool

Equal tests the passed-in JSON blobs to see if they are equal. It first compares the byte arrays directly. If they are not equal, it will call MakeIndex on them and then see if the JSON documents are structurally equal.

func GetBigInt added in v0.9.5

func GetBigInt(data []byte, keys ...string) (*big.Int, error)

GetBigInt returns the value retrieved by `Get`, cast to a *big.Int if possible. If key data type does not match, it will return an error. You shold use it if GetInt returns OverflowIntegerError.

func GetBoolean

func GetBoolean(data []byte, keys ...string) (val bool, err error)

GetBoolean returns the value retrieved by `Get`, cast to a bool if possible. The offset is the same as in `Get`. If key data type do not match, it will return error.

func GetFloat

func GetFloat(data []byte, keys ...string) (val float64, err error)

GetFloat returns the value retrieved by `Get`, cast to a float64 if possible. The offset is the same as in `Get`. If key data type do not match, it will return an error.

func GetInt

func GetInt(data []byte, keys ...string) (val int64, err error)

GetInt returns the value retrieved by `Get`, cast to an int64 if possible. If key data type does not match, it will return an error.

func GetNull

func GetNull(data []byte, keys ...string) error

GetNull returns an error if the value at keys is not a literal null.

func GetString

func GetString(data []byte, keys ...string) (val string, err error)

GetString returns the value retrieved by `Get`, cast to a string if possible, trying to properly handle escape and utf8 symbols If key data type do not match, it will return an error.

func IsNull

func IsNull(b []byte) bool

IsNull tests to see of a value is a literal null.

func ObjectEach

func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType) error, keys ...string) (err error)

ObjectEach iterates over the key-value pairs of a JSON object, invoking a given callback for each such entry

func ParseBigInt added in v0.9.5

func ParseBigInt(b []byte) (res *big.Int, err error)

ParseBigInt parses an integer of indefinite size and returns it. You should use it if ParseInt returns OverflowIntegerError, as that indicates that the value is out of the bounds of what an int64 can handle.

func ParseBoolean

func ParseBoolean(b []byte) (bool, error)

ParseBoolean parses a Boolean ValueType into a Go bool (not particularly useful, but here for completeness)

func ParseFloat

func ParseFloat(b []byte) (float64, error)

ParseNumber parses a Number ValueType into a Go float64

func ParseInt

func ParseInt(b []byte) (int64, error)

ParseInt parses a Number ValueType into a Go int64

func ParseString

func ParseString(b []byte) (string, error)

ParseString parses a String ValueType into a Go string (the main parsing work is unescaping the JSON string)

func Unescape

func Unescape(in, out []byte) ([]byte, error)

Unescape unescapes the string contained in 'in' and returns it as a slice. If 'in' contains no escaped characters:

Returns 'in'.

Else, if 'out' is of sufficient capacity (guaranteed if cap(out) >= len(in)):

'out' is used to build the unescaped string and is returned with no extra allocation

Else:

A new slice is allocated and returned.

Types

type Index

type Index interface {
	// WriteTo allows for low copy marshalling of an Index to something that
	// wants the raw JSON.  WriteTo always returns JSON in minified canonical form.
	// The returned byte array will contain a single line of JSON with no line ending, and all
	// object keys will be sorted according to their unescaped UTF-8 encoding.
	WriteTo(w io.Writer) (int64, error)
	// MarshalJSON does the same, except it returns just the raw bytes. It only returns
	// an error if you run out of memory, more or less.
	MarshalJSON() ([]byte, error)
	// Type returns the type of the object the Index references.
	Type() ValueType
	// Equal returns whether this Index is equal to another Index
	Equal(other Index) bool
	// Get retrieves the sub-value at keys, if the Index can do that.  If it cannot or there
	// is no such sub-value, it returns nil and KeyPathNotFoundError.  If keys is zero-length, it returns itself.
	Get(keys ...string) (Index, error)
	// Delete deletes the sub-value at keys.  If it does not exist, it is an error.
	Delete(keys ...string) error
	// Replace replaces the existing value at keys to value.  There must be a value at the target.
	Replace(value []byte, keys ...string) error
	// Add inserts the value at keys.
	// If the target is an array, the values in the array will be shifted to accommodate the new object.
	// If the target is an array, and the final key in the path is `-`, the value will be appended to the array.
	Add(value []byte, keys ...string) error
	// All returns an iterator over the Index, if the value is amenable to iteration. It does not iterate into sub-objects
	// for arrays and objects.
	All() iter.Seq2[string, Index]
}

Index abstracts out the structure of a JSON value and provides methods for searching and updating it without needing to reparse the underlying byte buffer over and over again.

func IndexValue

func IndexValue(buf []byte) (idx int, res Index, err error)

IndexValue creates an Index for the first JSON value in buf. If IndexValue returns without an error, then idx points to the first position in buf after the JSON value that was parsed.

If IndexValue returns with an error, idx points to the start of the JSON value that caused the parse error.

func MakeIndex

func MakeIndex(buf []byte) (res Index, err error)

MakeIndex takes a buffer full of JSON, parses it, and returns an Index that can be used for fast access to arbitrary parts of the JSON, memory-efficient updates, and fast remarshalling. The returned object references the passed-in buf for all of its data until you start modifying things, and is not safe for concurrent access from multiple goroutines.

If there are any errors during json parsing (due to bad JSON, usually), an error will be returned. This includes trailing non-whitespace in the passed-in byte array.

func MustIndex

func MustIndex(buf []byte) Index

MustIndex makes an Index out if the first JSON value in buf or panics.

type Op added in v0.9.3

type Op []byte

Op represents a valid JSON Patch operation as defined by RFC 6902

func (Op) From added in v0.9.3

func (o Op) From() Pointer

From returns the "from" pointer from the Op. Not all operations require a From pointer, so this will return nil if it is missing.

func (Op) Op added in v0.9.3

func (o Op) Op() string

Op returns the "op" field for this Op.

All Ops must have an operation, this method will panic if it is missing.

func (Op) Path added in v0.9.3

func (o Op) Path() Pointer

Path returns the JSON pointer for this Op.

All Ops must have a Path, this method will panic if it is missing. It assumes the pointer in the Op is well-formed.

func (Op) Valid added in v0.9.3

func (o Op) Valid() bool

Valid returns true if the op has all required keys for the operation it encodes and the values are valid.

func (Op) Value added in v0.9.3

func (o Op) Value() json.RawMessage

Value returns the JSON encoded value for this Op, or nil if there is no value.

type Patch

type Patch []byte

Patch is an array of individual JSON Patch operations. This patch supports the standard JSON patch operators, plus a `missing` operator that will error out if the JSON value contains anything at the specified `path`. This allows us to provide truly atomic and granular support for adding a value at a specified path.

func Generate

func Generate(base, target []byte, paranoid bool) (Patch, error)

Generate generates a JSON Patch that will modify base into target. If paranoid is true, then the generated patch with have test checks for changed item.

base and target must be byte arrays containing valid JSON

func GenerateFull

func GenerateFull(base, target []byte, paranoid, pretest bool) (Patch, error)

Generate generates a JSON Patch that will modify base into target. If paranoid is true, then the generated patch with have test checks for changed item. If pretest is true, then the generated patch with have test ALL parts of the base.

base and target must be byte arrays containing valid JSON

func MakePatch added in v0.9.2

func MakePatch(opts PatchOpts, base, target []byte) (Patch, error)

func NewPatch

func NewPatch(buf []byte) (res Patch, err error)

NewPatch takes a byte array and tries to unmarshal it.

func (Patch) Apply

func (p Patch) Apply(base []byte, buf []byte) (result []byte, err error, loc int)

ApplyJSON does the same thing as Apply, except the inputs should be JSON-containing byte arrays instead of unmarshalled JSON

func (Patch) MarshalJSON added in v0.9.3

func (p Patch) MarshalJSON() ([]byte, error)

func (Patch) Ops added in v0.9.3

func (p Patch) Ops() []Op

Ops returns the individual patch operations.

This is mostly present for debugging purposes.

func (*Patch) UnmarshalJSON added in v0.9.3

func (p *Patch) UnmarshalJSON(buf []byte) error

type PatchOpts added in v0.9.2

type PatchOpts struct {
	// TestIndividualChanges generates a `test` op for every `remove` or
	// `replace` op generated while testing differences in a json object.
	// `add` does not get a `test` by default because we only generate them
	// for objects on truly new fields, and testing the entire parent
	// object beforehand conflicts with the intent to make just the changes
	// we are protecting atomic. Truly granular testing would either need to
	// change the semantics of `add` on an object to fail if th
	TestIndividualChanges bool
	// SupportMissingOp will remedy the above issue with JSONPatch by
	// generating a non-standard `missing` op that passes if the path being
	// tested does not exist.
	SupportMissingOp bool
	// TestFullBase skips all other tests and just includes a `test` Op
	// against the full object at the beginning.  Only use when you really
	// want to guard the entire object instead of just the changes you want to make,
	// as the resulting patch may be too strict.
	TestFullBase bool
}

PatchOpts control how the patch will be generated.

type Patcher added in v0.9.3

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

Patcher is a helper object for generating JSON patches programatically without diffing two objects. An empty Patcher is ready for use.

func (*Patcher) Add added in v0.9.3

func (p *Patcher) Add(path Pointer, value interface{})

func (*Patcher) Append added in v0.9.3

func (p *Patcher) Append(other Patch)

func (*Patcher) Copy added in v0.9.3

func (p *Patcher) Copy(path, from Pointer)

func (*Patcher) Missing added in v0.9.3

func (p *Patcher) Missing(path Pointer)

func (*Patcher) Move added in v0.9.3

func (p *Patcher) Move(path, from Pointer)

func (*Patcher) Op added in v0.9.3

func (p *Patcher) Op(op string, path, from Pointer, value interface{})

Op adds a single operation to the patch-in-progress.

func (*Patcher) Patch added in v0.9.3

func (p *Patcher) Patch() (Patch, error)

Patch finalizes the JSON patch using the buffered Ops and returns it. If there was an error during patch generation, or the resulting patch was invalid, an error is returned indicating where along with the accumulated patch.

func (*Patcher) Remove added in v0.9.3

func (p *Patcher) Remove(path Pointer)

func (*Patcher) Replace added in v0.9.3

func (p *Patcher) Replace(path Pointer, value interface{})

func (*Patcher) Test added in v0.9.3

func (p *Patcher) Test(path Pointer, val interface{})

type Pointer

type Pointer []byte

Pointer is a JSON Pointer that conforms to RFC 6901, plus extensions that allow for indexing json arrays starting from the end using negative numbers.

func NewPointer

func NewPointer(s string) (Pointer, error)

NewPointer takes a string that conforms to RFC6901 and turns it into a JSON pointer. It returns an error if the passed-in string does not describe a valid JSON pointer.

func Ptr added in v0.9.3

func Ptr(s string) Pointer

Ptr does the same as NewPointer, except it will panic if the pointer is invalid.

func PtrTo added in v0.9.3

func PtrTo(s ...string) Pointer

PtrTo constructs a new Pointer out of the passed in Path fragments. It is the inverse operation to Pointer.Path()

func (Pointer) Append

func (p Pointer) Append(frag string) Pointer

Append adds a new segment to the end of the Pointer.

func (Pointer) Chop added in v0.9.1

func (p Pointer) Chop() (string, Pointer)

Chop extracts the last element in the pointer, returning it and the rest of the pointer.

func (Pointer) Contains

func (p Pointer) Contains(q Pointer) bool

Contains tests to see if pointer q refers to a value contained in p

func (Pointer) Equal added in v0.9.3

func (p Pointer) Equal(other Pointer) bool

Equal tests to see if one Pointer is equal to another.

func (Pointer) MarshalJSON

func (p Pointer) MarshalJSON() ([]byte, error)

MarshalJSON marshals a pointer into JSON.

func (Pointer) Path

func (p Pointer) Path() []string

Path returns a string array that is suitable to use as a parameter to the `keys ...string` arguments that many other methods and functions in this package take.

func (Pointer) Shift added in v0.9.1

func (p Pointer) Shift() (string, Pointer)

Shift extracts the first element in the pointer, returning it and the rest of the pointer.

func (Pointer) String

func (p Pointer) String() string

String takes a pointer and returns its string value.

func (*Pointer) UnmarshalJSON

func (p *Pointer) UnmarshalJSON(buf []byte) error

UnmarshalJSON unmarshals a pointer from JSON.

func (Pointer) Valid added in v0.9.3

func (p Pointer) Valid() bool

Valid tests to see if a Pointer has no improper `~` escapes and is otherwise well-formed.

type ValueType

type ValueType byte

Data types available in valid JSON data.

func Get

func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error)

Get - Receives data structure, and key path to extract value from.

Returns: `value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error `dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null` `offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper. `err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist`

Accept multiple keys to specify path to JSON value (in case of quering nested structures). If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation.

func (ValueType) String

func (vt ValueType) String() string

Jump to

Keyboard shortcuts

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