json

package module
v0.0.0-...-72eedf1 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

go-ruby-json/json

json — go-ruby-json

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's JSON parser and generator — matching MRI 4.0.5's JSON.parse, JSON.generate and JSON.pretty_generate byte-for-byte. It turns a JSON document into a tree of Ruby values and renders such a tree back to the exact bytes MRI's json gem produces — without any Ruby runtime.

It is the JSON backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-yaml (Psych).

Why not encoding/json? Ruby's JSON semantics differ in ways that matter: insertion-ordered objects, big-integer parsing, symbolize_names, the exact fpconv float layout, the JSON::ParserError / NestingError / GeneratorError taxonomy with MRI-exact messages (down to the at line L column C suffix), NaN/Infinity handling, and pretty_generate's precise spacing. This package ports MRI's behaviour directly rather than wrapping the standard library.

Features

Faithful port of MRI's JSON.parse + JSON.generate, validated against the ruby binary on every supported platform:

  • Full JSON grammar — objects, arrays, strings (with \u escapes and surrogate pairs), numbers (integers, big integers, floats and exponents), true / false / null.
  • Insertion-ordered objects — parsed objects come back as an ordered *Map so key order round-trips (last value wins on a duplicate key, MRI-style).
  • symbolize_names — object keys as Symbol instead of string.
  • MRI-exact errors — the JSON::ParserError / JSON::NestingError / JSON::GeneratorError / TypeError taxonomy, with the same messages and at line L column C positions; a nesting limit (default 100) on both parse and generate.
  • fpconv float formatting — the json gem's shortest-round-trip layout (2.0, 1e+15, 0.0000001, 5e-324), not Go's or Ruby's Float#to_s.
  • MRI string escaping\" \\ \b \f \n \r \t, \u00XX for other control characters, / not escaped, UTF-8 passed through verbatim.
  • pretty_generate — the exact two-space-indent layout, plus the individual JSON::State knobs (indent / space / space_before / object_nl / array_nl).
  • NaN / Infinity — a GeneratorError / ParserError by default, accepted as the bare NaN / Infinity / -Infinity tokens under allow_nan.

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x).

Install

go get github.com/go-ruby-json/json

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-json/json"
)

func main() {
	// Parse — objects come back as an ordered *json.Map.
	v, _ := json.Parse(`{"name":"web","ports":[80,443],"big":100000000000000000000}`)
	m := v.(*json.Map)
	ports, _ := m.Get("ports")
	fmt.Println(ports) // [80 443]

	// Generate — compact, MRI-byte-exact.
	out := json.NewMap()
	out.Set("checked", true)
	out.Set("ratio", 2.0)
	s, _ := json.Generate(out)
	fmt.Println(s) // {"checked":true,"ratio":2.0}

	// PrettyGenerate — JSON.pretty_generate layout.
	p, _ := json.PrettyGenerate([]any{int64(1), int64(2)})
	fmt.Println(p)
	// [
	//   1,
	//   2
	// ]

	// symbolize_names.
	sv, _ := json.Parse(`{"a":1}`, json.WithSymbolizeNames(true))
	fmt.Printf("%#v\n", sv.(*json.Map).Pairs()[0].Key) // "a" (json.Symbol)
}

Ruby value model

A Ruby value is an any drawn from a small, fixed set of Go types, so a host can map its own object graph to and from this package:

Ruby Go (Generate accepts) Go (Parse returns)
nil nil nil
true / false bool bool
Integer int, int64, *big.Int int64 / *big.Int
Float float64, float32 float64
String string string
Symbol json.Symbol json.Symbol (symbolize_names)
Array []any []any
Hash *json.Map, map[string]any, map[Symbol]any *json.Map (ordered)

A plain Go map is generated in sorted-key order; a *json.Map preserves insertion order, and Parse always returns objects as *json.Map.

API

// Parse parses a JSON document into a tree of Ruby values (JSON.parse).
func Parse(s string, opts ...Option) (any, error)
func ParseBytes(b []byte, opts ...Option) (any, error)

// Generate renders a Ruby value to a compact JSON document (JSON.generate).
func Generate(v any, opts ...Option) (string, error)

// PrettyGenerate renders with the JSON.pretty_generate layout.
func PrettyGenerate(v any, opts ...Option) (string, error)

func WithSymbolizeNames(on bool) Option // symbolize_names:
func WithMaxNesting(n int) Option       // max_nesting: (0 / negative = unlimited)
func WithAllowNaN(on bool) Option       // allow_nan:
func WithIndent(s string) Option        // indent:
func WithSpace(s string) Option         // space:
func WithSpaceBefore(s string) Option   // space_before:
func WithObjectNL(s string) Option      // object_nl:
func WithArrayNL(s string) Option       // array_nl:

type Symbol string
type Map    struct { /* insertion-ordered Hash */ }
func NewMap() *Map
func (m *Map) Set(key, val any)
func (m *Map) Get(key any) (any, bool)
func (m *Map) Pairs() []Pair
func (m *Map) Len() int

// Error taxonomy (each carries its Ruby exception class via RubyClass()).
type Error interface { error; RubyClass() string }
type ParserError    struct{ Message string } // JSON::ParserError
type NestingError   struct{ Message string } // JSON::NestingError
type GeneratorError struct{ Message string } // JSON::GeneratorError
type TypeError      struct{ Message string } // TypeError

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: a wide corpus is generated/parsed here and checked against the system ruby (JSON.generate / JSON.parse / JSON.pretty_generate and the error messages). The oracle reads its input on $stdin.binmode and writes on $stdout.binmode so Windows text-mode never mangles embedded newlines, and self-gates on MRI ≥ 4.0 (the JSON gem 4.0 is the reference), skipping where ruby is absent or older.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-json/json authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package json is a pure-Go (CGO-free) reimplementation of Ruby's JSON parser and generator, matching MRI 4.0.5's JSON.parse / JSON.generate / JSON.pretty_generate byte-for-byte. Parsing and generating JSON is fully deterministic — it needs no interpreter — so it lives here as pure Go: Parse turns a JSON document into a tree of Ruby values, and Generate / PrettyGenerate render such a tree back to the exact bytes MRI's json gem produces.

It is the JSON backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-yaml (Psych).

Ruby value model

A Ruby value is represented by an [any] drawn from a small, fixed set of Go types so a host (such as go-embedded-ruby) can map its own object graph to and from this package:

Ruby            Go (Generate accepts)            Go (Parse returns)
----            ---------------------            ------------------
nil             nil                              nil
true / false    bool                             bool
Integer         int, int64, *big.Int             int64 or *big.Int
Float           float64, float32                 float64
String          string                           string
Symbol          Symbol                           Symbol (symbolize_names)
Array           []any                            []any
Hash            *Map (ordered), map[...]any      *Map (insertion order)

Parse returns mappings as an ordered *Map so key order is preserved (MRI keeps it); Generate accepts a *Map, or a plain Go map (emitted in sorted-key order for determinism).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Generate

func Generate(v Value, opts ...Option) (string, error)

Generate renders a Ruby value to a compact JSON document, matching JSON.generate / Object#to_json. The value is drawn from the package value model; a non-finite float without WithAllowNaN returns a *GeneratorError, and over-deep nesting a *NestingError.

func GenerateSource

func GenerateSource(src Source, opts ...Option) (string, error)

GenerateSource renders src to a compact JSON document by pulling its values through an Encoder, the streaming counterpart of Generate (no intermediate value tree). The options behave exactly as for Generate.

func ParseInto

func ParseInto(s string, dst Builder, opts ...Option) error

ParseInto parses a JSON document straight into the host Builder dst, reproducing JSON.parse semantics (and its errors) but with no intermediate tree of this package's values — the parser drives dst as it reads, so a host such as go-embedded-ruby materialises its own object graph in a single allocation-light pass. On success dst.Result() holds the top-level value; on a malformed document, nesting overflow or non-string input the matching error is returned and dst's result is unspecified.

func PrettyGenerate

func PrettyGenerate(v Value, opts ...Option) (string, error)

PrettyGenerate renders a Ruby value with MRI's JSON.pretty_generate layout: two-space indent, "\n" object/array newlines and a single space after ':'. Any WithIndent / WithSpace / WithObjectNL / WithArrayNL options override those defaults.

func PrettyGenerateSource

func PrettyGenerateSource(src Source, opts ...Option) (string, error)

PrettyGenerateSource is GenerateSource with MRI's pretty_generate layout (two-space indent, "\n" newlines, a space after ':'), overridable by opts.

Types

type Builder

type Builder interface {
	// Null appends a JSON null.
	Null()
	// Bool appends a JSON true/false.
	Bool(b bool)
	// Int appends an integer that fits in int64.
	Int(n int64)
	// Big appends an integer too large for int64 (never nil).
	Big(n *big.Int)
	// Float appends a floating-point number.
	Float(f float64)
	// Str appends a string value (an object value or array element, never a key).
	Str(s string)
	// BeginArray opens an array; n is a capacity hint for pre-allocation, or 0
	// when the count is not known up front (the default parser passes 0 and sizes
	// each container exactly at EndArray instead of pre-scanning for the count). A
	// Builder that pre-sizes must tolerate n == 0 by growing on demand. The
	// emitted values up to the matching EndArray are its elements.
	BeginArray(n int)
	// EndArray closes the array opened by the matching BeginArray.
	EndArray()
	// BeginObject opens an object; n is a capacity hint, or 0 when unknown (see
	// BeginArray). It is followed by (Key, value) sequences up to EndObject.
	BeginObject(n int)
	// Key sets the key for the next emitted value. symbolize reports whether the
	// host should intern it as a Symbol (MRI's symbolize_names: true).
	Key(s string, symbolize bool)
	// EndObject closes the object opened by the matching BeginObject.
	EndObject()
	// Result returns the single top-level value once parsing completes.
	Result() Value
}

Builder is a streaming sink the parser drives as it reads a document, so a host (such as go-embedded-ruby) can construct its own object graph directly — with no intermediate tree of this package's `any` values, and no second conversion pass. ParseInto feeds a Builder; Parse is ParseInto over the package's own [valueBuilder].

The parser calls the scalar methods (Null/Bool/Int/Big/Float/Str) for leaf values and the container methods (BeginArray/EndArray and BeginObject/Key/ EndObject) around composites. Every emitted value — a scalar, or the array or object just closed — becomes the *current value*; the host attaches it to its enclosing container (the most recent open array element, or the value of the most recent object Key) on the next call, or returns it from Builder.Result at the top level. The call sequence is well-formed by construction: containers nest correctly, an object alternates Key then value, and exactly one value is emitted at the top level.

A Builder that pre-sizes its containers (BeginArray/BeginObject carry the element count) and interns small scalars turns parsing into a single allocation-light pass.

type Encoder

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

Encoder is the generate-side façade a Source writes through. It wraps the streaming generator, so a host emits a value with one call and the bytes, nesting limit, MRI-faithful formatting and pretty-print layout are handled here.

func (*Encoder) Array

func (e *Encoder) Array(n int, emit func() error) error

Array emits a JSON array of n elements (n is the exact count, used only for the array_nl / indent layout decisions — it is not relied on for correctness). emit is called once and must push exactly n element values via the encoder; it honours the configured nesting limit and pretty-print layout. A nil emit (or n == 0) writes an empty array.

func (*Encoder) Big

func (e *Encoder) Big(n *big.Int)

Big emits an arbitrary-precision integer.

func (*Encoder) Bool

func (e *Encoder) Bool(b bool)

Bool emits true or false.

func (*Encoder) Elem

func (e *Encoder) Elem()

Elem must be called once per array element by the Array emit callback, before emitting that element, so the comma and newline separators (and the per-line indent) land between elements exactly as MRI lays them out.

func (*Encoder) Float

func (e *Encoder) Float(f float64) error

Float emits a float with MRI's fpconv layout, returning a GeneratorError for a non-finite value unless allow_nan is configured.

func (*Encoder) Int

func (e *Encoder) Int(n int64)

Int emits an integer that fits in int64.

func (*Encoder) Key

func (e *Encoder) Key(s string)

Key emits an object key (the pair separator, indent, the quoted key and the space/colon) before its value, and must be called once per pair by the Object emit callback, immediately before emitting that pair's value. The key is rendered as a JSON string (MRI coerces every object key to a string).

func (*Encoder) Null

func (e *Encoder) Null()

Null emits a JSON null.

func (*Encoder) Object

func (e *Encoder) Object(n int, emit func() error) error

Object emits a JSON object of n pairs (n is the exact count, used only for the object_nl / indent layout). emit is called once and must push exactly n pairs via Encoder.Key then a value. A nil emit (or n == 0) writes an empty object.

func (*Encoder) Str

func (e *Encoder) Str(s string)

Str emits a string value with MRI's JSON string escaping.

type Error

type Error interface {
	error
	// RubyClass is the fully-qualified Ruby exception class this error maps to
	// (e.g. "JSON::ParserError").
	RubyClass() string
}

Error is the base of this package's error taxonomy, mirroring MRI's JSON::JSONError hierarchy. Every error returned by Parse, Generate and PrettyGenerate is one of *ParserError, *NestingError, *GeneratorError or *TypeError; all satisfy Error. RubyClass reports the matching Ruby exception class name so a host (go-embedded-ruby) can raise the right Ruby exception.

type GeneratorError

type GeneratorError struct{ Message string }

GeneratorError is raised when a value cannot be generated — a non-finite float without allow_nan: MRI's JSON::GeneratorError.

func (*GeneratorError) Error

func (e *GeneratorError) Error() string

func (*GeneratorError) RubyClass

func (e *GeneratorError) RubyClass() string

type Map

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

Map is an insertion-ordered Ruby Hash. Parse returns objects as *Map so key order round-trips; Generate accepts *Map, or a plain Go map (emitted in sorted key order).

The identity index is built lazily: while a Map holds at most [mapIndexThreshold] pairs a linear scan resolves a key faster than a hash lookup and needs no map allocation, so the overwhelmingly common small object carries no per-object map. The index is materialised only once a Map grows past the threshold, restoring O(1) lookup for large hashes.

func NewMap

func NewMap() *Map

NewMap returns an empty ordered Map.

func (*Map) Get

func (m *Map) Get(key Value) (Value, bool)

Get returns the value for key and whether it was present.

func (*Map) Len

func (m *Map) Len() int

Len reports the number of entries.

func (*Map) Pairs

func (m *Map) Pairs() []Pair

Pairs returns the entries in insertion order. The slice must not be mutated.

func (*Map) Set

func (m *Map) Set(key, val Value)

Set inserts or replaces the entry for key. A later equal key replaces the earlier entry's value in place (MRI's last-wins on duplicate object keys), keeping the original position.

type NestingError

type NestingError struct{ Message string }

NestingError is raised when nesting exceeds the limit, on both parse and generate: MRI's JSON::NestingError (a subclass of JSON::ParserError in MRI; we model it as its own type and report it as the more specific class).

func (*NestingError) Error

func (e *NestingError) Error() string

func (*NestingError) RubyClass

func (e *NestingError) RubyClass() string

type Option

type Option func(*config)

Option configures Parse, Generate and PrettyGenerate, mirroring the keyword options of MRI's JSON.parse / JSON.generate / JSON.pretty_generate. An option that does not apply to a given call is simply ignored (MRI tolerates the same).

func WithAllowNaN

func WithAllowNaN(on bool) Option

WithAllowNaN permits the non-finite floats NaN, Infinity and -Infinity in both directions (MRI's allow_nan:). On Generate they emit the bare NaN / Infinity / -Infinity tokens; on Parse those bare tokens are accepted. Without it, a non-finite float is a GeneratorError and the tokens are a ParserError.

func WithArrayNL

func WithArrayNL(s string) Option

WithArrayNL sets the newline string emitted after each array delimiter and between elements (MRI's array_nl:). PrettyGenerate defaults it to "\n".

func WithIndent

func WithIndent(s string) Option

WithIndent sets the per-level indent string used by generation (MRI's indent:). PrettyGenerate defaults it to two spaces.

func WithMaxNesting

func WithMaxNesting(n int) Option

WithMaxNesting sets the maximum structure depth for Parse and Generate (MRI's max_nesting:). A value of 0 (or negative) disables the limit, matching MRI's max_nesting: false / 0.

func WithObjectNL

func WithObjectNL(s string) Option

WithObjectNL sets the newline string emitted after each object delimiter and between pairs (MRI's object_nl:). PrettyGenerate defaults it to "\n".

func WithSpace

func WithSpace(s string) Option

WithSpace sets the string emitted after the ':' separating an object key from its value (MRI's space:). PrettyGenerate defaults it to a single space.

func WithSpaceBefore

func WithSpaceBefore(s string) Option

WithSpaceBefore sets the string emitted before the ':' in an object pair (MRI's space_before:). Default empty.

func WithSymbolizeNames

func WithSymbolizeNames(on bool) Option

WithSymbolizeNames makes Parse return object keys as Symbol instead of string (MRI's symbolize_names: true). It has no effect on Generate.

type Pair

type Pair struct {
	Key Value
	Val Value
}

Pair is one entry of an ordered mapping.

type ParserError

type ParserError struct{ Message string }

ParserError is a malformed-document error: MRI's JSON::ParserError. Its message matches MRI's, including the "at line L column C" suffix.

func (*ParserError) Error

func (e *ParserError) Error() string

func (*ParserError) RubyClass

func (e *ParserError) RubyClass() string

type Source

type Source interface {
	EmitTo(e *Encoder) error
}

Source is a value a host can render to JSON by pushing it into an Encoder, the mirror of Builder on the generate side: instead of first converting its object graph into this package's value model and handing that to Generate, the host walks its own graph once and calls the Encoder's typed methods. This removes the per-node intermediate value and its type-switch.

GenerateSource / PrettyGenerateSource call EmitTo with a configured Encoder; EmitTo emits exactly one value (a scalar, or one Array/Object whose body emits its elements). A non-nil error (e.g. a non-finite float without allow_nan, surfaced by Encoder.Float) aborts generation and is returned to the caller.

type Symbol

type Symbol string

Symbol is a Ruby Symbol (`:name`). Generate emits it as its bare name (a JSON string of the name); Parse yields Symbol keys when WithSymbolizeNames(true) is set (MRI's symbolize_names: true).

type TypeError

type TypeError struct{ Message string }

TypeError mirrors MRI raising a plain TypeError when Parse is handed a non-String (e.g. JSON.parse(123)).

func (*TypeError) Error

func (e *TypeError) Error() string

func (*TypeError) RubyClass

func (e *TypeError) RubyClass() string

type Value

type Value = any

Value is the interface satisfied by every Ruby value this package handles. It is purely documentary — the public API uses any — but a host may use it to constrain its own adapters.

func Parse

func Parse(s string, opts ...Option) (Value, error)

Parse parses a JSON document into a tree of Ruby values, matching JSON.parse. Objects come back as an ordered *Map (key order preserved), arrays as []any, numbers as int64 / *big.Int / float64, and strings as Go strings (with WithSymbolizeNames, object keys come back as Symbol). A malformed document returns a *ParserError; exceeding the nesting limit a *NestingError; a non-string input a *TypeError.

func ParseBytes

func ParseBytes(b []byte, opts ...Option) (Value, error)

ParseBytes is Parse for a byte slice (MRI accepts a String either way).

Jump to

Keyboard shortcuts

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