Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 ./...

About

Mutable, ordered JSON for Go with path-based access, typed conversions, exact number preservation, and iterator support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages