Help us shape the future of Go! Take the 2026 Go Developer Survey

djson

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 16 Imported by: 0

README

djson

Go Reference

Mutable, ordered JSON for Go — useful when decoding into structs is too rigid and editing a map[string]any is too awkward.

djson keeps object keys in insertion order, provides path-based access and typed conversions, preserves parsed number spelling, and still works with encoding/json.

Why djson?

  • Ordered objects. Parsing, mutation, cloning, and serialization preserve key order.
  • Mutable tree. Read and update nested values without declaring a struct for every document shape.
  • Convenient paths. Use paths such as items/0/price across objects and arrays.
  • Exact parsed numbers. JSON numbers remain json.Number, including their original decimal and exponent notation.
  • Strict boundaries. Invalid JSON returns an error; invalid mutations panic instead of leaving a partially valid tree.
  • Standard-library integration. DynamicJSON implements json.Marshaler and json.Unmarshaler.
  • Read-only sharing. Freeze makes a complete tree safe for concurrent readers.
  • Modern iteration. Immediate and recursive traversal are available, along with Go iterator sequences.

Install

go get github.com/gavriva/djson

Quick start

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/gavriva/djson"
)

func main() {
	document, err := djson.Parse([]byte(`{
		"request":{"id":"r-17"},
		"items":[{"name":"pen","qty":2}]
	}`))
	if err != nil {
		log.Fatal(err)
	}

	qty := document.GetInt("items/0/qty", 0)
	document.Set("items/0/qty", qty+1)
	document.Set("request/processed", true)

	item := djson.NewMap()
	item.SetKey("name", "paper")
	item.SetKey("qty", 1)
	document.Array("items").Append(item)

	encoded, err := json.Marshal(document)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(encoded))
}

Output:

{"request":{"id":"r-17","processed":true},"items":[{"name":"pen","qty":3},{"name":"paper","qty":1}]}

Objects keep their order

Updating a key does not move it. Deleting and inserting it again places it at the end.

object := djson.NewMap()
object.SetKey("second", 2)
object.SetKey("first", 1)
object.SetKey("second", 20)

fmt.Println(string(object.JSONLine()))
// {"second":20,"first":1}

This is useful for deterministic output, human-facing configuration, logs, hashes, and signatures over serialized JSON.

Paths and literal keys

Most accessors accept slash-separated paths:

document.Set("users/0/name", "Ada")
name := document.GetString("users/0/name", "unknown")
value, exists := document.Fetch("users/0/name")

When Set creates a missing level, canonical decimal segments such as 0 and 12 create arrays. Segments with leading zeroes, such as 007, remain object keys.

A literal object key may contain /, ~, or even be empty. Use the raw-key methods when a key must not be interpreted as a path:

document.SetKey("a/b", 1)
document.SetKey("", 2)

fmt.Println(document.GetKey("a/b"))
fmt.Println(document.HasKey(""))
document.DeleteKey("a/b")

The raw-key family is GetKey, HasKey, SetKey, DeleteKey, and NestedKey.

Typed access

Scalar getters convert common JSON representations and return the supplied default when conversion is impossible:

workers := document.GetInt("config/workers", 4)
ratio := document.GetFloat("config/ratio", 1.0)
enabled := document.GetBool("config/enabled", false)
label := document.GetString("config/label", "default")
timeout := document.GetDuration("config/timeout", 5*time.Second)
createdAt := document.GetTime("created_at")

ToInt, ToFloat, ToBool, and ToString provide the same conversions for standalone values. GetAnySlice preserves every array element, while typed slice getters fail loudly when an element cannot be converted. GetSlice and Each are intended for arrays containing objects, arrays, or JSON null.

Number and time representation

The tree uses a small canonical set of scalar representations:

Input Stored representation
Parsed JSON number json.Number, original spelling preserved
Signed Go integer int64
Unsigned Go integer uint64
float64 float64
float32 json.Number formatted with float32 precision
time.Duration duration string, for example "250ms"
time.Time UTC RFC3339 string

NaN and infinities become JSON null because JSON has no representation for them. An invalid json.Number is rejected at mutation time.

IsEqual compares numbers by value, so equivalent representations such as json.Number("1.0"), int64(1), and float64(1) compare equal. Object order does not affect structural equality.

Iteration

Use EachPair for immediate values:

for key, value := range document.EachPair("request") {
	fmt.Printf("%s = %v\n", key, value)
}

Iterate offers callback-based immediate traversal, and Visit recursively visits Get-compatible paths. Array keys are canonical decimal indexes in all of these APIs.

Iteration has deterministic mutation rules:

  • values appended after iteration starts are not visited;
  • replacements inside the initial boundary are visible;
  • object keys deleted before their turn are skipped;
  • deleting or clearing an array during iteration panics because it would shift active indexes.

Serialization

compact := document.JSONLine()
pretty := document.JSON()

compactWithError, err := json.Marshal(document)

JSONLine returns compact JSON and JSON returns indented JSON. Both panic on cycles or excessive nesting. Use json.Marshal or call MarshalJSON directly when serialization failures must be returned as errors.

UnmarshalJSON atomically replaces the receiver and may switch it between an object and an array. Top-level JSON null is a no-op.

Freezing and cloning

DynamicJSON is not safe for concurrent access involving mutation. Freeze a fully built document before sharing it between readers:

document.Freeze()
// Concurrent reads are now safe. Any mutation attempt panics.

Freezing is recursive and irreversible. Clone returns a mutable deep copy and does not copy the frozen state. If a source tree shares a child in several places, each place receives an independent cloned child.

Performance profile

djson is aimed at frequently accessed small and medium dynamic documents. It uses the Segment JSON tokenizer for parsing, a dedicated ordered serializer, and lazy object indexes: small objects stay as compact ordered slices, while an index is built when an object grows.

The repository includes benchmarks for parsing, path operations, serialization, cloning, equality, iteration, and flat and branching documents. Run them on the target machine instead of relying on portable-looking nanosecond claims.

Parsing and limits

Parse accepts a top-level object or array. Unlike encoding/json, it rejects top-level scalars. It validates strict JSON grammar, preserves key order, and copies the input; the complete source copy may remain referenced for the lifetime of the parsed tree.

Nesting is limited by MaxNestingDepth:

  • Parse returns an error when untrusted input is too deep;
  • recursive in-memory operations panic when the limit is exceeded;
  • iterative mutation may build a deeper document, but a later recursive operation will reject it.

djson is an in-memory mutable tree. For very large documents or streaming workloads, a token-based decoder is usually a better fit.

Files and HTTP responses

FromFile reads an object or array from disk. FromResponse and FromResponse200 parse HTTP response bodies, close the body, understand identity and gzip content encodings, and limit the decoded body to MaxResponseBodySize.

The difference between the response helpers is status handling:

  • FromResponse may return both a parsed non-200 response and a status error;
  • FromResponse200 returns a nil document for non-200 responses.

Deliberate boundaries

The package models JSON, not arbitrary Go object graphs:

  • Set, SetI, and Append accept JSON scalars, slices, arrays, string-keyed maps, and *DynamicJSON values;
  • custom MarshalJSON and MarshalText implementations are not invoked;
  • json.RawMessage, arbitrary pointers, structs, functions, channels, complex numbers, and maps with non-string keys are rejected;
  • inserting an existing *DynamicJSON can create a cycle; serialization and recursive traversal detect it instead of overflowing the stack.

Unsupported leaf values fail at the mutation boundary rather than much later during output. Recursive operations separately enforce the nesting limit.

Development

go test ./...
go test -race ./...
go test -bench . -benchmem ./...

Documentation

Overview

Package djson provides mutable, ordered JSON objects and arrays.

DynamicJSON is not safe for concurrent use involving mutation. After Freeze returns, the complete tree is read-only and safe for concurrent readers. Freezing is irreversible, while Clone returns a mutable copy.

Mutation normalizes signed integers to int64, unsigned integers to uint64, float64 values to float64, and float32 values to json.Number. Parsed numbers remain json.Number to preserve their spelling. time.Duration values are stored as strings, and time.Time values as UTC RFC3339 strings.

Parse accepts only an object or array at the top level, unlike encoding/json, which also accepts scalar JSON values. Parse reports excessive nesting as an error; recursive operations such as serialization, Clone, Freeze, Visit, and IsEqual panic when MaxNestingDepth is exceeded.

Index

Constants

View Source
const MaxNestingDepth = 1000

MaxNestingDepth limits Parse and recursive operations. Parse returns an error at the limit; recursive operations panic. Mutable construction may exceed it, but the next recursive operation will panic.

View Source
const MaxResponseBodySize int64 = 16 << 20

MaxResponseBodySize limits decoded bodies read by the response helpers.

Variables

This section is empty.

Functions

func ToBool

func ToBool(v any) (bool, bool)

ToBool converts booleans, strconv.ParseBool strings, and numeric zero or one. The second result reports success.

func ToFloat

func ToFloat(v any, defaultValue float64) float64

ToFloat converts numbers and numeric strings to float64. It returns defaultValue on failure, NaN, or infinity.

func ToInt

func ToInt(v any, defaultValue int) int

ToInt converts numbers and numeric strings to int, rounding halves away from zero. It returns defaultValue on failure or overflow.

func ToString

func ToString(v any, defaultValue string) string

ToString formats scalars and time.Time; containers become compact JSON. It returns defaultValue for nil, unsupported values, NaN, or infinity.

Types

type DynamicJSON

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

DynamicJSON is a mutable, ordered JSON object or array.

func FromFile

func FromFile(filepath string) (r *DynamicJSON, err error)

FromFile reads and parses a JSON object or array from filepath.

func FromResponse

func FromResponse(resp *http.Response, err0 error) (*DynamicJSON, error)

FromResponse closes resp.Body and parses a limited identity or gzip body. On non-200 status it returns both the parsed document and a status error; an invalid error body joins the status and parse errors.

func FromResponse200

func FromResponse200(resp *http.Response, err0 error) (*DynamicJSON, error)

FromResponse200 is the strict response helper: non-200 status returns a nil document and an error. It closes resp.Body and accepts limited identity or gzip bodies.

func NewArray

func NewArray() *DynamicJSON

NewArray returns an empty JSON array.

func NewMap

func NewMap() *DynamicJSON

NewMap returns an empty JSON object.

func Parse

func Parse(data []byte) (*DynamicJSON, error)

Parse parses a top-level object or array, preserving key order and number spelling. Scalars and excessive nesting return errors. Parse copies data; tokens may reference that complete copy for the lifetime of the tree.

func (*DynamicJSON) Append

func (d *DynamicJSON) Append(v any)

Append adds an array value. Values appended during iteration are not visited.

func (*DynamicJSON) Array

func (d *DynamicJSON) Array(name string) *DynamicJSON

Array returns the child array named by an object key or array index, replacing another value with a new array when needed.

func (*DynamicJSON) ArrayI

func (d *DynamicJSON) ArrayI(i int) *DynamicJSON

ArrayI returns or creates the array at position i. Arrays grow as needed; object positions must exist. Negative and missing object positions panic.

func (*DynamicJSON) Clear

func (d *DynamicJSON) Clear()

Clear removes every value. Clearing an object during iteration is allowed; entries not yet visited are skipped. Clearing an array during iteration panics because removing its elements would shift active indexes.

func (*DynamicJSON) Clone

func (d *DynamicJSON) Clone() *DynamicJSON

Clone returns a mutable deep copy. Frozen state is not copied, and shared subtrees are duplicated independently.

func (*DynamicJSON) Delete

func (d *DynamicJSON) Delete(path string)

Delete removes the value at path. Deleting object keys during iteration is allowed, and keys deleted before they are visited are skipped. Deleting an existing array element during iteration panics because it would shift active indexes. Missing paths are no-ops.

func (*DynamicJSON) DeleteKey

func (d *DynamicJSON) DeleteKey(key string)

DeleteKey removes an immediate object key without parsing it as a path. Missing keys are no-ops; array and frozen receivers panic.

func (*DynamicJSON) Each

func (d *DynamicJSON) Each(path string) iter.Seq[*DynamicJSON]

Each iterates container and null elements of the array at path. Missing or null is empty; scalars and non-array values panic. Additions are not visited, replacements are visible, and Delete or Clear on the array panics.

func (*DynamicJSON) EachPair

func (d *DynamicJSON) EachPair(path string) iter.Seq2[string, any]

EachPair iterates immediate values at path. Object keys are unchanged; array keys are decimal indexes. Additions are not visited, replacements are visible, and deleted object keys are skipped. Delete and Clear panic on an active array.

func (*DynamicJSON) Fetch

func (d *DynamicJSON) Fetch(path string) (any, bool)

Fetch returns the value at path and whether it exists.

func (*DynamicJSON) Freeze

func (d *DynamicJSON) Freeze()

Freeze irreversibly makes the complete tree read-only. A frozen tree is safe for concurrent readers; mutation attempts panic.

func (*DynamicJSON) Get

func (d *DynamicJSON) Get(path string) any

Get returns the value at path, or nil when missing. Use Fetch to distinguish a missing value from JSON null.

func (*DynamicJSON) GetAnySlice

func (d *DynamicJSON) GetAnySlice(path string) []any

GetAnySlice returns a shallow copy of the array at path without converting its elements. Scalars, DynamicJSON containers, and JSON null values are preserved. A missing path, JSON null at path, or a non-array value returns nil; an empty array returns a non-nil empty slice.

func (*DynamicJSON) GetBool

func (d *DynamicJSON) GetBool(path string, defaultValue bool) bool

GetBool converts the value at path with ToBool or returns defaultValue.

func (*DynamicJSON) GetBoolsSlice

func (d *DynamicJSON) GetBoolsSlice(path string) []bool

GetBoolsSlice converts an array with ToBool. Missing or null returns nil; an empty array is non-nil. Non-array or unconvertible elements, including null, panic.

func (*DynamicJSON) GetDuration

func (d *DynamicJSON) GetDuration(path string, defaultValue time.Duration) time.Duration

GetDuration parses a duration string or interprets a JSON number as seconds. It returns defaultValue for missing, invalid, or overflowing values.

func (*DynamicJSON) GetFloat

func (d *DynamicJSON) GetFloat(path string, defaultValue float64) float64

GetFloat converts the value at path with ToFloat or returns defaultValue.

func (*DynamicJSON) GetFloatsSlice

func (d *DynamicJSON) GetFloatsSlice(path string) []float64

GetFloatsSlice converts an array with ToFloat. Missing or null returns nil; an empty array is non-nil. Non-array or unconvertible elements, including null, panic.

func (*DynamicJSON) GetI

func (d *DynamicJSON) GetI(i int) any

GetI returns the value at position i, or nil when unavailable. A negative position panics.

func (*DynamicJSON) GetInt

func (d *DynamicJSON) GetInt(path string, defaultValue int) int

GetInt converts the value at path with ToInt or returns defaultValue.

func (*DynamicJSON) GetIntsSlice

func (d *DynamicJSON) GetIntsSlice(path string) []int

GetIntsSlice converts an array with ToInt. Missing or null returns nil; an empty array is non-nil. Non-array or unconvertible elements, including null, panic.

func (*DynamicJSON) GetKey

func (d *DynamicJSON) GetKey(key string) any

GetKey returns an immediate object value without parsing key as a path. It returns nil when unavailable; use HasKey to distinguish JSON null.

func (*DynamicJSON) GetSlice

func (d *DynamicJSON) GetSlice(path string) []*DynamicJSON

GetSlice returns container and null elements from the array at path. Missing or null returns nil; an empty array returns a non-nil empty slice. Scalars and non-array values panic.

func (*DynamicJSON) GetStr

func (d *DynamicJSON) GetStr(path string) string

GetStr returns the string stored at path, without conversion, or "".

func (*DynamicJSON) GetString

func (d *DynamicJSON) GetString(path string, defaultValue string) string

GetString converts the value at path with ToString or returns defaultValue.

func (*DynamicJSON) GetStringsSlice

func (d *DynamicJSON) GetStringsSlice(path string) []string

GetStringsSlice converts an array with ToString. Missing or null returns nil; an empty array is non-nil. Non-array or unconvertible elements, including null, panic. Containers become compact JSON.

func (*DynamicJSON) GetTime

func (d *DynamicJSON) GetTime(path string) time.Time

GetTime parses an RFC3339 string or returns the zero time.

func (*DynamicJSON) Has

func (d *DynamicJSON) Has(path string) bool

Has reports whether path exists. A value containing JSON null still exists.

func (*DynamicJSON) HasKey

func (d *DynamicJSON) HasKey(key string) bool

HasKey reports whether an immediate object key exists without interpreting the key as a path. It returns false for an array or nil receiver.

func (*DynamicJSON) IsArray

func (d *DynamicJSON) IsArray() bool

IsArray reports whether d is an array; nil is not an array.

func (*DynamicJSON) IsEqual

func (d *DynamicJSON) IsEqual(o *DynamicJSON) bool

IsEqual reports structural equality. Numerically equal Go and JSON number types compare equal despite representation differences.

func (*DynamicJSON) IsFrozen

func (d *DynamicJSON) IsFrozen() bool

IsFrozen reports whether modifications to this value are prohibited. It returns false for a nil receiver.

func (*DynamicJSON) Iterate

func (d *DynamicJSON) Iterate(cb func(key string, value any) bool)

Iterate visits immediate values in object insertion or array order until cb returns false. Array keys are decimal indexes. Additions are not visited; replacements are visible and deleted object keys are skipped. Delete and Clear panic on an array being iterated.

func (*DynamicJSON) JSON

func (d *DynamicJSON) JSON() []byte

JSON returns an indented JSON representation. A nil receiver is serialized as null. It panics if the value contains a cycle or exceeds MaxNestingDepth; call MarshalJSON directly when these failures must be returned as errors.

func (*DynamicJSON) JSONLine

func (d *DynamicJSON) JSONLine() []byte

JSONLine returns a compact JSON representation. A nil receiver is serialized as null. It panics if the value contains a cycle or exceeds MaxNestingDepth; call MarshalJSON directly when these failures must be returned as errors.

func (*DynamicJSON) Keys

func (d *DynamicJSON) Keys() []string

Keys returns object keys in insertion order or canonical array indexes.

func (*DynamicJSON) Len

func (d *DynamicJSON) Len() int

Len returns the number of object entries or array elements; nil has length 0.

func (*DynamicJSON) Map

func (d *DynamicJSON) Map(name string) *DynamicJSON

Map returns the child object named by an object key or array index, replacing another value with a new object when needed.

func (*DynamicJSON) MapI

func (d *DynamicJSON) MapI(i int) *DynamicJSON

MapI returns or creates the object at position i. Arrays grow as needed; object positions must exist. Negative and missing object positions panic.

func (*DynamicJSON) MarshalJSON

func (d *DynamicJSON) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. It preserves object key order and the original representation of json.Number values. Cycles and values exceeding MaxNestingDepth are returned as errors.

func (*DynamicJSON) Nested

func (d *DynamicJSON) Nested(path string) *DynamicJSON

Nested returns the object or array at path, or nil for a missing or scalar value.

func (*DynamicJSON) NestedI

func (d *DynamicJSON) NestedI(i int) *DynamicJSON

NestedI returns the container at position i, or nil when unavailable. A negative position panics.

func (*DynamicJSON) NestedKey

func (d *DynamicJSON) NestedKey(key string) *DynamicJSON

NestedKey returns the container at an immediate object key without parsing the key as a path, or nil when unavailable.

func (*DynamicJSON) Set

func (d *DynamicJSON) Set(path string, value any)

Set writes value at a slash-separated path, creating missing containers. During automatic creation, a missing container becomes an array only when the next path segment is a supported canonical decimal index such as "0" or "12". Segments with leading zeros, such as "007", are object keys. A canonical index longer than nine digits panics before modifying the tree. The type of an existing container always takes precedence over this rule.

func (*DynamicJSON) SetI

func (d *DynamicJSON) SetI(i int, value any)

SetI sets position i. Arrays grow with null-filled gaps; objects preserve the existing key. Invalid positions and nil or frozen receivers panic.

func (*DynamicJSON) SetKey

func (d *DynamicJSON) SetKey(key string, value any)

SetKey sets an immediate object key without interpreting the key as a path. It panics when the receiver is nil, an array, or frozen.

func (*DynamicJSON) UnmarshalJSON

func (d *DynamicJSON) UnmarshalJSON(data []byte) error

UnmarshalJSON atomically replaces d with an object or array and may change its kind. JSON null is a no-op; other scalars are unsupported. Frozen or actively iterated receivers return an error.

func (*DynamicJSON) Visit

func (d *DynamicJSON) Visit(cb func(path string, value any))

Visit recursively visits values using Get-compatible paths. Additions are not visited; replacements are visible and deleted object keys are skipped. Delete and Clear panic on an active array. Cycles and excessive depth panic.

Jump to

Keyboard shortcuts

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