Home

Awesome

JSON Benchmarks

Each of the charts below show the performance across several different JSON implementations:

The Go toolchain used is v1.21.1.

Based on the module proxy as of 2023-07-01, the relative popularity of each:

Note that JSONv2 deliberately dissuades users from depending on the package as it is an experiment and is subject to major breaking changes.

Benchmarks were run across various datasets:

All of the implementations other than JSONv1 and JSONv2 make extensive use of unsafe. As such, we expect those to generally be faster, but at the cost of memory and type safety. SonicJSON goes a step even further and uses just-in-time compilation to generate machine code specialized for the Go type being marshaled or unmarshaled. Also, SonicJSON does not validate JSON strings for valid UTF-8, and so gains a notable performance boost on datasets with multi-byte Unicode. Benchmarks are performed based on the default marshal and unmarshal behavior of each package. Note that JSONv2 aims to be safe and correct by default, which may not be the most performant strategy.

JSONv2 has several semantic changes relative to JSONv1 that impacts performance:

  1. When marshaling, JSONv2 no longer sorts the keys of a Go map. This will improve performance.

  2. When marshaling or unmarshaling, JSONv2 always checks to make sure JSON object names are unique. This will hurt performance, but is more correct.

  3. When marshaling or unmarshaling, JSONv2 always shallow copies the underlying value for a Go interface and shallow copies the key and value for entries in a Go map. This is done to keep the value as addressable so that JSONv2 can call methods and functions that operate on a pointer receiver. This will hurt performance, but is more correct.

All of the charts are unit-less since the values are normalized relative to JSONv1, which is why JSONv1 always has a value of 1. A lower value is better (i.e., runs faster).

Benchmarks were performed on an AMD Ryzen 9 5900X.

Marshal Performance

Concrete types

Benchmark Marshal Concrete

Interface types

Benchmark Marshal Interface

RawValue types

Benchmark Marshal Rawvalue

Unmarshal Performance

Concrete types

Benchmark Unmarshal Concrete

Interface types

Benchmark Unmarshal Interface

RawValue types

Benchmark Unmarshal Rawvalue

Streaming

When reading from an io.Reader and writing to an io.Writer, a JSON implementation should not need a buffer much larger than the largest JSON token encountered within the entire JSON value. For example, marshaling and unmarshaling a [{},{},{},{},{},...] that is a gigabyte in size should not need to buffer the entire JSON array, but only enough to buffer each individual { or }. An implementation with true streaming support will use a fixed amount of memory regardless of the total size of the JSON value.

The following implementations have true streaming support:

ImplementationMarshalUnmarshal
JSONv1
JSONv2✔️✔️
JSONIterator✔️
SegmentJSON
GoJSON
SonicJSON

See TestStreaming for more information.

Correctness

A package may be fast, but it must still be correct and realiable.

UTF-8 Validation

According to RFC 8259, section 8.1, a JSON value must be encoded using UTF-8.

The following table shows how each implementation handles invalid UTF-8:

ImplementationMarshalUnmarshal
JSONv1⚠️ replaced⚠️ replaced
JSONv2✔️ rejected✔️ rejected
JSONIterator⚠️ replaced❌ ignored
SegmentJSON⚠️ replaced⚠️ replaced
GoJSON⚠️ replaced❌ ignored
SonicJSON❌ ignored❌ ignored

Notes:

See TestValidateUTF8 for more information.

Duplicate Object Names

RFC 8259, section 4 specifies that handling of a JSON object with duplicate names results in undefined behavior where compliant parsers may use the first member, the last member, all the members, or report an error. RFC 7493, section 2.3 specifies that JSON objects must not have duplicate names. Rejecting duplicate object names is more correct, but incurs a performance cost verifying this property.

The following table shows how each implementation handles duplicate object names:

ImplementationMarshalUnmarshal
JSONv1❌ allowed❌ allowed
JSONv2✔️ rejected✔️ rejected
JSONIterator❌ allowed❌ allowed
SegmentJSON❌ allowed❌ allowed
GoJSON❌ allowed❌ allowed
SonicJSON❌ allowed❌ allowed

See TestDuplicateNames for more information.

Parsing Test Suite

"Parsing JSON is a Minefield 💣" (posted 2016-10-26) performed one of the first thorough comparisons of JSON parsers and their behavior on various edge-cases. At the time, RFC 7159 was the authoritative standard, but has since been superseded by RFC 8259. Consequently, the expected results of some of the test cases from the article were changed to be more compliant with RFC 8259.

The following table shows the number of test case failures for each implementation when tested against RFC 8259:

ImplementationStringNumberObjectArrayOther
JSONv1❌ 10x✔️✔️✔️✔️
JSONv2✔️✔️✔️✔️✔️
JSONIterator❌ 10x❌ 4x✔️✔️✔️
SegmentJSON❌ 10x✔️✔️✔️✔️
GoJSON❌ 30x❌ 52x❌ 20x❌ 17x❌ 10x
SonicJSON❌ 28x✔️✔️❌ 1x✔️

RFC 7493 is compatible with RFC 8259 in that it makes strict decisions about behavior that RFC 8259 leaves undefined. In particular, it rejects escaped surrogate pairs that are invalid and rejects JSON object with duplicate names.

The following table shows additional test case failures for each implementation when tested against RFC 7493:

ImplementationStringNumberObjectArrayOther
JSONv1❌ 9x✔️❌ 3x✔️✔️
JSONv2✔️✔️✔️✔️✔️
JSONIterator❌ 9x✔️❌ 3x✔️✔️
SegmentJSON❌ 9x✔️❌ 3x✔️✔️
GoJSON❌ 9x✔️❌ 3x✔️✔️
SonicJSON❌ 9x✔️❌ 3x✔️✔️

See TestParseSuite for more information.

MarshalJSON Validation

A JSON implementation should not trust that the output of a MarshalJSON method is valid JSON nor formatted in the same way as surrounding JSON. Consequently, it should parse and reformat the JSON output to be consistent.

The following table shows which implementations validate MarshalJSON output:

ImplementationValidates
JSONv1✔️ yes
JSONv2✔️ yes
JSONIterator❌ no
SegmentJSON✔️ yes
GoJSON✔️ yes
SonicJSON✔️ yes

See TestValidateMarshalJSON for more information.

Deterministic Map Ordering

RFC 8259 specifies that JSON objects are an "unordered collection". Thus, a compliant JSON marshaler need not serialize Go maps entries in any particular order.

The JSONv1 implementation historically sorted keys, which consequently set the precedence for other JSON implementations to do likewise. The JSONv2 implementation no longer sorts keys for better performance and because it does not violate any specified facet of correctness.

The following table shows which implementations deterministically marshal maps:

ImplementationDeterministic
JSONv1✔️ yes
JSONv2❌ no
JSONIterator❌ no
SegmentJSON✔️ yes
GoJSON✔️ yes
SonicJSON❌ no

See TestMapDeterminism for more information.

Observable Changes With Unmarshal Errors

Implementations differ regarding how much of the output value is modified when an unmarshaling error is encountered.

There are generally two reasonable behaviors:

  1. Make no mutating changes to the output if the input is invalid.
  2. Make as many changes as possible up until the input becomes invalid.

The following table shows what changes are observable if the input is invalid:

ImplementationObservable Changes
JSONv1✔️ none
JSONv2⚠️ all
JSONIterator⚠️ all
SegmentJSON❌ some
GoJSON❌ some
SonicJSON⚠️ all

See TestUnmarshalErrors for more information.

Binary Size

For use in embedded or mobile applications, a small binary size is a priority. The following table shows the binary sizes of each JSON implementation for a simple Go program that just links in json.Marshal and json.Unmarshal. These were built with GOOS=linux and GOARCH=amd64.

ImplementationSize
JSONv12.18 MiB
JSONv22.89 MiB
JSONIterator3.11 MiB
SegmentJSON2.52 MiB
GoJSON3.39 MiB
SonicJSON24.7 MiB

See TestBinarySize for more information.