Skip to content

Latest commit

 

History

History
198 lines (168 loc) · 12.9 KB

File metadata and controls

198 lines (168 loc) · 12.9 KB

serde-json

For general-purpose JSON processing where performance, CPU pressure, memory pressure, or minimum latency aren't primary concerns, serde_json is the go-to crate. Its major strengths include integration with the serde ecosystem and the ability to map from JSON text into Rust data structures and from Rust data structures to JSON text with minimal code. It supports serialization and writing JSON, not just parsing it.

On the parsing performance side, bufjson provides about 1.35X more throughput than serde_json on general-purpose, single-threaded, JSON parsing. The bufjson advantage grows in highly concurrent multi-tenant contexts like web servers, because allocators are inherently contended resources and bufjson can be used with zero or near-zero allocation. More subtly, bufjson can be used without creating memory pressure. The trade-off to be noted is that serde_json is, in some ways, doing more work than bufjson1, since it is allocating memory for strings, expanding escape sequences in strings, and converting numbers to Rust types, whereas bufjson defers all this work unless it is specifically requested. Which library is faster ultimately depends on the specific application.

Feature comparison

Feature serde_json bufjson
Token-level pull parsing
Faster parse
Concatenated JSON/JSONL
Minimize copy on read
Minimize allocation on read
Syntax validation without allocation
Precise line, column, and offset of every token
Async/incremental input
Streaming JSON Pointer evaluation
Lossless parsing2
Arbitrary number values
Structured errors with source location
serde integration
Schemaless in-memory tree (Value)
Map JSON into Rust types
Write/serialize
no_std

Notes

  1. In other ways, bufjson is doing more work than serde_json. Specifically, it performs lossless parsing, tracks precise parsing locations, and provides rich error messages.
  2. Lossless parsing means parsed JSON text can be re-serialized exactly the same way it appeared in the input. This allows JSON text to be edited, e.g., by making spot insertions, deletions, or edits, without changing the rest of the text.

simd-json

For pure blazing-fast JSON syntax parsing nothing beats the SIMD-accelerated simd-json crate. Parsing throughput on a fixed-size buffer is the main reason to use this crate, as it performs about 1.74X faster than bufjson and 2.35X faster than serde_json on eligible pure-parsing use cases. (When simd-json is used to build an in-memory tree, which bufjson does not do, bufjson is 1.14X faster, though this comparison is admittedly apples-to-oranges.)

Some limitations of simd-json are:

  • It cannot parse incrementally: it needs to see the entire input buffer at once.
  • It irreversibly mutates the input buffer.
  • It cannot parse JSON text larger than ~4 GiB.
  • In addition to the input buffer, it requires memory of about 2X the size of the input buffer to complete the parse. If the input buffer has to be cloned to make it mut, it requires 3X the size.

Feature comparison

Feature simd-json bufjson
Token-level pull parsing
Faster parse
Concatenated JSON/JSONL1 🟡
Minimize copy on read
Minimize allocation on read
Syntax validation without allocation
Precise line, column, and offset of every token
Async/incremental input
Streaming JSON Pointer evaluation
Lossless parsing2
Arbitrary number values3
Structured errors with source location4
serde integration
Schemaless in-memory tree (Value)
Map JSON into Rust types
Write/serialize
no_std

Notes

  1. simd-json can be used to handle delimited formats like JSONL/NDJSON, provided you read the entire input into a buffer and pre-process it to find the delimiters. This approach will fall down if the input file is too large to read into memory. simd-json cannot easily be used to handle non-delimited formats.
  2. Lossless parsing means parsed JSON text can be re-serialized exactly the same way it appeared in the input. This allows JSON text to be edited, e.g., by making spot insertions, deletions, or edits, without changing the rest of the text.
  3. simd-json eagerly converts numbers into i64, u64, or f64. Numbers whose magnitudes are out of range cause parse errors; and there is no way to obtain the exact token text for in-range numbers that can't be represented without loss of precision.
  4. simd-json can give the byte offset of the error position for some errors, but reports index 0 for many structural errors. It never provides the human-readable line and column numbers.

json-streaming

The json-streaming crate appeared around the same time as the first versions of bufjson. It targets some of the same use cases. However, bufjson is the superior alternative because it has: markedly better performance in all scenarios, a more correct implementation1, and a more flexible API. The one exception is for no-alloc use cases, since bufjson has a no_std configuration but does not support no-alloc.

In parsing performance, bufjson is a notable 3.8X faster than the json-streaming ordinary blocking parser and a dramatic 36X faster than the json-streaming non-blocking parser which is intended for async use cases. The bufjson approach to async is expected to be nearly as fast as the synchronous parsing case, with only minimal throughput given up due to async machinery overhead.2

Feature comparison

Feature json-streaming bufjson
Token-level pull parsing
Faster parse
Concatenated JSON/JSONL
Minimize copy on read
Minimize allocation on read
Syntax validation without allocation
Precise line, column, and offset of every token3
Async/incremental input
Streaming JSON Pointer evaluation
Lossless parsing4
Arbitrary number values
Structured errors with source location5
serde integration
Schemaless in-memory tree (Value)
Map JSON into Rust types
Write/serialize
no_std

Notes

  1. As of v1.0.3, json-streaming does not handle Unicode escape sequence surrogate pairs correctly. When detecting this valid scenario, it records a parse error due to the invalid UTF-8 it generates and aborts.
  2. On the topic of async, writing an appropriate general-purpose API for an asynchronous JSON parser is admittedly a challenge. Given that asynchronous file I/O isn't performant in Rust, it is submitted that files should be read synchronously, e.g. by bufjson::lexical::read::ReadAnalyzer; and that async parsing should be limited to network stack use cases. Here, bufjson's approach of consuming zero-copy Bytes values via bufjson::lexical::pipe::PipeAnalyzer is expected to encounter far less overhead from the async machinery than the approach of using a general-purpose non-blocking reader.
  3. The method JsonReader::location returns the current reader position, which is after the end of the current token and can refer to a different line number than the one on which the token appeared.
  4. The json-streaming parser does not provide access to whitespace and eagerly unescapes strings.
  5. While json-streaming does give fairly accurate error locations, its errors are effectively "stringly-typed".

struson

The struson crate has similar capabilities to json-streaming in a slightly different API. It targets similar use cases. One interesting struson parsing feature that isn't widely available is the ability to configure the parser to recognize and ignore end of line and block comments in JSON text of the type supported by VS Code; a second related feature is the ability to configure the parser to ignore trailing commas within object and array values.

In parsing performance, bufjson is about 6X faster than struson. If pure streaming JSON throughput is important, bufjson is the preferable alternative.

Feature comparison

Feature struson bufjson
Token-level pull parsing1
Faster parse
Concatenated JSON/JSONL
Minimize copy on read
Minimize allocation on read
Syntax validation without allocation
Precise line, column, and offset of every token
Async/incremental input
Streaming JSON Pointer evaluation
Lossless parsing2
Arbitrary number values
Structured errors with source location
serde integration
Schemaless in-memory tree (Value)
Map JSON into Rust types
Write/serialize
no_std

Notes

  1. While struson::reader::Reader::peek can tell you what type of value comes next, this is at value level, one step higher up than token level.
  2. The struson parser does not provide access to individual tokens or whitespace and eagerly unescapes strings.