PEGO

JSON

github.com/ornew/pego/parsers/json parses JSON as defined by RFC 8259, with a parser generated by PEGO from json.pego. It depends only on the standard library and the PEGO runtime.

# In your module, with a checkout of the PEGO repository at ~/src/pego:
go work init .
go work use ~/src/pego ~/src/pego/parsers/json

Until PEGO publishes a release, a parser module depends on the github.com/ornew/pego module through a local replace directive, so go get cannot fetch it from outside a checkout of the repository. The install notes show the replace form too.

Use

import "github.com/ornew/pego/parsers/json/types"

// Go values, as encoding/json decodes into an any: map[string]any, []any, string, float64, bool, nil.
v, err := json.Decode(`{"name": "pego", "stars": 42}`)

// Typed values with positions.
ast, err := json.ParseAST(`{"a": [1, true], "b": "x\ty"}`)
for _, m := range ast.(*types.Object).Members {
	fmt.Printf("%q at %d-%d: %T\n", m.Key.Value(), m.Span.Start, m.Span.End, m.Value)
}
// "a" at 1-15: *types.Array
// "b" at 17-28: *types.String

// Only check.
ok := json.Valid(`[1, 2, 3,]`) // false

// Syntax errors.
_, err = json.ParseAST(`{"a": [1, 2,]}`)
var se *json.SyntaxError
if errors.As(err, &se) {
	fmt.Println(se.Line, se.Col, se.Message())
	// 1 13 syntax error: expected "-", "0", "[", "\"", "false", "null", "true", "{", (? \t\r\n), (?1-9)
}
ParseAST(input, opts...) (types.Value, error) The value: *types.Object, *types.Array, *types.String, *types.Number, *types.Bool or *types.Null, each with its Span
Decode(input) (any, error) Go values, as encoding/json decodes into an any
ToAny(types.Value) (any, error) The conversion Decode uses
Valid(input) bool, Recognize(input, opts...) error Only check the input
Parse(input, opts...) (*Node, error) The tree of *Node, as the engine returns it
(*types.String).Value() string The string with its escapes decoded (Text is the source between the quotes)
(*types.Number).Float64(), (*types.Number).Int64() The number (Text is the source)
(*types.Bool).Value() bool The boolean

Positions are in code points by default; json.ParseAST(src, json.WithUnit(json.Bytes)) counts bytes.

Conformance

The parser accepts exactly the JSON texts of RFC 8259: any value at the top level, surrounded by optional whitespace. The tests check:

  • every case of JSONTestSuite (MIT License, fetched by go run ./hack/corpora fetch jsontestsuite; the tests skip without it): the 95 y_ files are accepted and decode to what encoding/json decodes, the 188 n_ files are rejected, and Recognize and ParseAST agree on the 35 i_ files;
  • Decode against encoding/json on 2,000 random documents, and on arbitrary input in FuzzDecode (go test -fuzz FuzzDecode): both must accept the same inputs and return the same values.

Where RFC 8259 leaves the behavior to the implementation, the package behaves like encoding/json:

  • invalid UTF-8 in strings is accepted and decodes to U+FFFD, and so does an unpaired surrogate escape ("\ud800");
  • of duplicate keys, Decode keeps the last (ParseAST keeps them all, in order);
  • a number outside the range of float64 (1e400) is a valid *Number but an error in Decode and Float64;
  • a byte order mark is rejected.

Nesting is limited by the generated parser's depth limit of 100,000 rule calls, about 33,000 levels of arrays (where encoding/json stops at 10,000); deeper input fails with an error instead of exhausting the stack.

Performance

On an array of objects of 256 KB (go test -bench .), ParseAST is about a sixth faster than encoding/json decoding into an any, and allocates a quarter more bytes in 230 times fewer allocations; Decode (which converts the typed values) takes about 15% longer than encoding/json. Valid builds nothing, but it is not faster than ParseAST and far slower than encoding/json's Valid, which is a hand-written scanner. See the benchmarks of PEGO for the other backends.