Qubit JSON
Resource-aware JSON infrastructure for Rust services, configuration readers,
and data pipelines. It complements serde_json by admitting JSON under
caller-chosen limits before untrusted input consumes unbounded parsing,
materialization, or output resources, while preserving Serde's data model.
Use JsonDecoder when the input must already be strict JSON, or
NormalizingJsonDecoder when the boundary explicitly permits controlled
cleanup of JSON embedded in external text.
Installation
For the 0.12 version described by this checkout:
[]
= "0.12"
= { = "0.10", = ["json"] }
= "1.0.135"
Local checkout:
[]
= { = "0.12", = "../rs-json" }
= { = "0.10", = ["json"] }
= "1.0.135"
Add serde = { version = "1.0", features = ["derive"] } when decoding into
derived application types.
To enable RFC 8785 canonical output, opt into the feature explicitly:
= { = "0.12", = ["jcs"] }
Quick start: decode an already aggregated, size-limited HTTP body
This example decodes bytes from an HTTP body that the transport layer has
already aggregated under a size limit. It accepts a full-range u64
identifier, then demonstrates that oversized JSON input is rejected before a
decoded value is committed:
use JsonDecodeLimits;
use JsonResource;
use JsonDecodeError;
use JsonDecodeErrorKind;
use JsonDecoder;
Unlike calling serde_json::from_slice alone, this boundary makes resource
admission explicit and exposes a stable error category through kind(). Apply
the transport size limit before aggregating the body into the slice shown
above. The application flow is: bounded transport aggregation → JSON admission
with JsonDecoder → schema, authorization, and domain validation.
qubit-json does not provide HTTP body aggregation. For a downstream HTTP
integration that applies the transport limit before BoundedJson performs JSON
admission, see the rs-web controller example.
This is a changeable downstream reference; the user guide
explains decode_input.
decode_value_utf8 returns a protected SafeJsonValue owner; use as_value()
to borrow its tree. This entry point rejects duplicate object keys. The owner
protects cleanup of the decoded tree, but it does not replace an application
limit on the HTTP body before aggregation or promise an exact heap limit.
Admission limits and object/array checks describe the source JSON. A generic
Serde target may construct a different value; for example, ordinary
serde_json::Value can reinterpret a first $serde_json::private::RawValue
key when raw_value is enabled. Direct SafeJsonValue deserialization also
cannot identify private number maps; with arbitrary_precision, numeric shapes
can differ by feature configuration. These private-protocol behaviors apply to
generic Serde targets, not decode_value_utf8. See the user guide's
duplicate-key section before relying
on literal object preservation.
Choose the decoding boundary
Choose the entry point from the input contract, not from whether the payload happens to parse:
| Input contract | API |
|---|---|
| Input must already be complete, strict JSON | JsonDecoder |
| An owned document must reject duplicate keys and preserve native numeric events across feature unification | JsonDecoder::decode_unique_str / decode_unique_utf8 / decode_unique_seed_utf8 |
| A byte-oriented source should be read through EOF into an owned result | JsonDecoder::decode_input |
| Specific presentation artifacts are allowed before strict decoding | NormalizingJsonDecoder with an explicit policy |
| Normalized text must be inspected, decoded repeatedly, or borrowed by the result | NormalizingJsonDecoder::prepare_str / prepare_utf8, followed by NormalizingJsonDecoder::decode_precharged_document, decode_precharged_object_document, or decode_precharged_array_document |
The default normalization policy can trim surrounding whitespace, remove one BOM, unwrap one outer JSON Markdown fence, and escape raw ASCII control characters inside strings. It is controlled normalization, not another JSON dialect: comments, trailing commas, unquoted keys, and missing syntax remain errors.
Canonical bytes for a signed payload
When an application needs stable bytes for a hash or signature, enable jcs
and pass an already constructed serde_json::Value to JcsEncoder:
[]
= { = "0.12", = ["jcs"] }
use ;
use JcsEncoder;
The jcs feature is disabled by default. JcsEncoder canonicalizes the
Value model; it does not accept arbitrary Serialize values or raw JSON
text. With JcsDecoder, raw input duplicate keys are rejected before a
Value is built; once duplicates are collapsed into a map, they cannot be
recovered. JCS numbers use IEEE 754 binary64 semantics, so
9007199254740993 can be emitted as 9007199254740992; encode an integer as a
JSON string when its exact decimal digits must be preserved. The regular
JsonDecoder does not accept every JCS number lexeme as raw text.
For raw JSON text with JCS number semantics, enable jcs and use
JcsDecoder. Prefer decode_value_str, decode_value_utf8, or
decode_value_input when the result may be deeply nested: each returns a
SafeJsonValue. The older decode_str, decode_utf8, and decode_input
methods remain available for compatibility and return a bare
serde_json::Value.
use JcsDecoder;
use JcsEncoder;
All JCS decode entry points round numbers to finite binary64 and reject keys
that are duplicates after JSON unescaping. See the user guide
for finite-budget usage and rounding, Unicode, depth, and input-buffering
boundaries. JcsDecoder parses input; it does not verify that the input bytes
are already canonical. JcsEncoder still produces canonical output.
Why this project exists
Valid JSON can still be too large, too deep, or too expensive to materialize.
qubit-json keeps JSON syntax and Serde compatibility while letting callers
bound raw and normalized input, nesting, nodes, collection sizes, keys, strings,
numbers, payload, and encoded output. It also makes cumulative accounting and
commit boundaries explicit through qubit-budget sessions and transactions.
What it provides
| Domain | Use it for | Boundary |
|---|---|---|
decode |
Strict JSON admission, explicitly configured normalization, or JCS text decoding with JcsDecoder |
Ordinary decoding defaults to Integer64; JCS decoding uses binary64 and rejects duplicate keys |
encode |
Budgeted strict JSON output or JCS canonical bytes | Value accounting commits after a complete successful operation; an I/O failure can still leave bytes in an external writer |
format |
Validate and pretty-print JSON while preserving source tokens | Only whitespace changes; number/string spellings and duplicate keys are preserved without constructing a value tree |
value |
Protected SafeJsonValue trees and measured projection from Serde events |
A Serde seed cannot inspect original number text or enforce text-level range rules |
value::traverse |
Iterative reads or in-place mutations of materialized values | Mutation is incremental; visitors adding descendants must bound their own work, and visitor/output-budget failures retain prior changes |
qubit-budget owns limits, resource identities, budgets, and sessions.
qubit-json owns JSON normalization, lexical validation, text codecs, value
construction, and traversal.
Core types and API shapes
The public API centers on codecs, owned documents, and tree visitors. In the
table below, Value means serde_json::Value, T is an application type, and
the arrows show successful input/output shapes; fallible operations return
Result.
| Public API | Input → output | State and intended use |
|---|---|---|
decode::JsonDecoder |
&str / &[u8] → T or SafeJsonValue; byte input → owned result |
Retains a cumulative decode session and diagnostic policy. Use decode_str / decode_utf8 for Serde targets, decode_value_* for a protected tree that rejects duplicate keys, or validate_* for validation only. |
decode::NormalizingJsonDecoder |
External text → T, or prepare_* → NormalizedJsonDocument |
Applies an explicit NormalizingJsonDecodePolicy before strict decoding and retains a cumulative session. One-shot decoding requires DeserializeOwned. |
decode::NormalizedJsonDocument<'input> |
Prepared document → normalized &str or later decoded T |
Retains borrowed or owned text via Cow<'input, str>, plus original and normalized byte lengths. Use document-based decoding when results must borrow normalized text. |
encode::JsonEncoder |
&T: Serialize → Vec<u8> or a byte output |
Retains a cumulative encode session. Provides to_vec, write_buffered, and write_incremental for strict JSON output. |
value::JsonValueEncoder |
&T: Serialize → Value; transaction-aware projection → SafeJsonValue |
Stateless projection into a strict JSON tree. The optional encode_in_transaction_with_private_text_limit bounds each private payload independently; see the projection guide. |
value::SafeJsonValue |
Value → protected owner; as_value → &Value; into_value → Value |
Owns a tree and uses explicit stacks for its copy, comparison, and cleanup operations. new takes ownership without performing budget admission. |
value::traverse::JsonTreeReader |
&Value + JsonTreeVisitor → enter/leave callbacks |
Borrows a value transaction and measures admitted nodes during traversal. account measures a tree without callbacks. |
value::traverse::JsonTreeMutator |
&mut Value + JsonTreeMutVisitor → modified tree |
Borrows separate input/output value transactions. Checks input, runs mutations, then checks output; a later failure retains earlier mutations. |
decode::JcsDecoder / encode::JcsEncoder (jcs) |
decode_value_*: text → SafeJsonValue (legacy decode_* → Value); &Value → canonical bytes |
Decode with finite binary64 numbers and duplicate-key rejection; encode under RFC 8785 with a separate auxiliary working-capacity limit. |
format::JsonPrettyOptions + format_json_tokens / write_json_tokens |
JSON bytes → formatted bytes or a byte output | Configure indentation, displayed indentation depth, and the final newline. Formatting preserves scalar tokens and does not canonicalize them. |
The underlying tree remains Serde's familiar data model:
Value = Null | Bool | Number | String | Array(Vec<Value>) | Object(Map<String, Value>)
For example, an application can compose these APIs without changing tree models:
Strict JSON ── JsonDecoder::decode_value_* ──> SafeJsonValue
│ as_value()
v
&Value ── JsonTreeReader ──> visitor callbacks
&T: Serialize ── JsonValueEncoder::encode_in_transaction ──> SafeJsonValue
│
v
JsonEncoder ──> JSON bytes
│
v
format_json_tokens
Codec type parameters follow the shape <'budget, R = JsonResource, Q = usize>:
'budget describes a borrowed budget's lifetime, R identifies resources, and
Q represents quantities. Construct codecs with with_limits for an owned
session or new for a supplied session; usage accumulates across calls on the
same session. Value transactions stage accounting until the caller commits.
Mutable visitors return JsonTreeControl::Descend or SkipSubtree and receive
a JsonTreeContext containing root-inclusive depth and the node's location in
its immediate parent. Error handling uses structured types such as
JsonDecodeError and JsonEncodeError, with stable categories exposed by
kind().
See the user guide for protected-tree composition and the advanced usage section for normalization, transactions, and visitors.
Ordinary decoding defaults to JsonDecodeNumberModel::Integer64; the optional
finite fallback and JCS binary64 contracts are described in the
JSON number contract.
For JCS, Budget identifies value or output limits, while WorkingMemory
identifies the separate auxiliary-capacity limit configured with
JcsEncoder::with_limits(limits, max_working_bytes).
Explicit boundaries
- Ordinary strict admission validates JSON syntax and the documented numeric
range; it does not require object keys to be unique. Choose a target such as
DuplicateKeyRejectingJsonValue, or useJcsDecoderfor raw JCS text, when uniqueness is part of the contract. - Ordinary text decoding defaults to
Integer64: negative integers fiti64and non-negative integers fitu64. Explicit fallback controls admission of wider integer tokens, but the typed input path leaves representation toTand activeserde_jsonfeatures; underarbitrary_precision, a preserved wide integer can still fail strict re-encoding. Validated decoding withJsonNumberPolicy::Preservematerializes a fallback integer as finitef64, with possible rounding.JcsDecoderconverts every number to finite binary64, so9007199254740993becomes9007199254740992. Use strings or domain types when every decimal digit must be preserved. JsonValueEncoderrecognizes only the exact privateNumberSerde struct protocol; an ordinary map with the same field name remains an object. MalformedRawValueandNumberprotocols returnJsonSerializationErrorKind::InvalidSerializerState, withJsonSerializerStateError::InvalidRawValueProtocolorInvalidNumberProtocolrespectively. Seed-based value construction only sees native numeric events and cannot infer private map provenance.JsonDecoder::decode_value_*returns aSafeJsonValuewhose own tree operations and cleanup use an explicit stack. The configured, root-inclusivemax_depthis the depth limit; ordinarydecode_*<T>still relies on the target type and does not promise arbitrary-depth construction or destruction.JcsDecoderretains its historical depth-error variant, but has no separate fixed 127-container limit. Itsdecode_value_str,decode_value_utf8, anddecode_value_inputmethods returnSafeJsonValuedirectly and are the recommended entry points for deep trees. Only the compatibility methodsdecode_str,decode_utf8, anddecode_inputreturn bareserde_json::Value; if using one of them, wrap a deep result immediately withSafeJsonValue::newbefore retaining or dropping it.decode_unique_validated_*appliesJsonValidatedDecodeOptionsand calls the supplied number policy with each original number token and its path. The extra node/string limits apply per document;JsonDecodeSessionresource usage accumulates across calls. Policy callback side effects are not rolled back if a later syntax error occurs.JsonValueEncoder::encode_in_transactionstages measurements in a caller-owned value transaction and returns a protected owner. The optionalencode_in_transaction_with_private_text_limitbounds each private RawValue/Number payload's UTF-8 source bytes independently; this is not cumulative, does not constrain ordinary strings, does not affect the older entry point, and is separate fromJsonEncoder's output-byte limit. OnPrivateTextLimitExceeded, drop the entire transaction. See the projection guide. Passing the owner toJsonEncoderand thenformat_json_tokenscomposes value projection, strict JSON output, and token formatting without changing the numeric model.format_json_tokensvalidates JSON syntax and preserves number/string token spellings and duplicate keys; it does not materialize aValueor canonicalize the document.JsonSerializationErrorKind::InvalidRawValuereports rejection by the strict raw-value materialization contract, which includes more than syntax errors. Ordinary newtype structs remain transparent; useserde_json::value::RawValuewhen raw JSON materialization is intended.- Tree traversal counts an existing
serde_json::Numberusing the length of its current compact display. Underarbitrary_precision, this permits accounting for a stored wide number such as1e400; ordinary and JCS encoders still reject numbers outside their own finite range. - Set finite limits at every untrusted boundary.
unlimited()disables value, output, and working-memory limits; use it only for trusted input or data already admitted by another layer. An outer output bound cannot replace input and structural admission before parsing. - Slice-based strict and normalizing decoder methods consume the complete
supplied
&stror&[u8]; their input-byte limits admit that slice and do not limit memory already allocated by a transport or body aggregator. StrictJsonDecoderslice methods can return values borrowing from the input. One-shotNormalizingJsonDecodermethods requireDeserializeOwned, since normalized text may be temporary. To borrow after normalization, prepare aNormalizedJsonDocumentand decode from it; borrowed results live no longer than that document.JsonDecoder::decode_inputcontinues to read anInput<Item = u8>source through EOF and return an ownedDeserializeOwnedvalue. It buffers the complete document; withoutmax_input_bytes, that temporary memory use has no configured limit. Apply a bounded read or body-aggregation limit before decoding. This strict JSON method is not a raw-text JCS canonicalization entry point. JcsDecoder::decode_inputalso reads and buffers the complete input. With a finite input limit, it may consume one uncharged probe byte that cannot be returned to the source. Lexical budgets are not exact heap-memory bounds: result-container and input-buffer capacities and allocator metadata are not measured precisely. JCS Value construction uses the shared builder and the configured root-inclusivemax_depth; there is no separate fixed container cap. The legacyMaterializationDepthLimitvariant remains for source compatibility, while configured depth rejection is reported asDecodewith a budget error. Thedecode_value_str,decode_value_utf8, anddecode_value_inputmethods returnSafeJsonValuedirectly and are the recommended entry points for deep trees. Only the compatibility methodsdecode_str,decode_utf8, anddecode_inputreturn a bareValue; if using one of them, wrap a deep result immediately withSafeJsonValue::new. Duplicate keys are compared after JSON unescaping, without Unicode normalization; canonical-byte verification is outside this API.JcsEncoder::write_bufferedandwrite_incrementalcan leave and charge bytes accepted by an external destination before a later write or flush failure. Incremental mode writes directly and flushes on success; value accounting is committed only after the complete operation succeeds.max_working_bytesbounds auxiliary traversal and object-sorting capacity, not the inputValue, allocator metadata, or output buffer capacity. Value admission runs before rendering, and its traversal stack is outside this working-memory limit. When that traversal needs a depth bound, configuremax_depthalongsidemax_nodes; the working-memory setting describes renderer collection capacity, not an exact heap ceiling.- Diagnostics are redacted by default. Configure a strict decoder with
JsonDecoder::with_diagnostic_policy, or a normalizing decoder through its normalization policy. Redacted errors retain only stable classifications and source coordinates: they never retain an offending byte, token/key/value text, or parser/Serde source. EnableDiagnosticPolicy::Detailedonly where input-derived sources are safe to retain and log. This does not redact errors returned by validated number policy callbacks: the outer error hides them inDisplay/Debug, but preserves them in its source chain. Callers must choose safe policy error contents and logging; see Errors and diagnostics.
Learn more
- English user guide · 中文用户手册
- JSON number contract · JSON 数字契约
- Design documents · 设计文档
- Benchmark guide · 基准测试指南
- Released API documentation; for the current
branch API, run
cargo doc --all-features --open
Testing
--all-features covers this root crate's declared features. The compatibility
matrix also runs the root crate tests against the serde_json 1.0.135 baseline
and the latest resolvable version, with eight combinations of jcs,
serde_json/arbitrary_precision, and serde_json/preserve_order. The baseline
is 1.0.135 because serde_json::Map::into_values() first appears there;
this is the lowest compatible version established by that API boundary. If an
existing Cargo.lock pins an older release, run cargo update -p serde_json
before using the --locked verification commands. The minimum does not require
enabling arbitrary_precision or preserve_order. The rust,ignore examples
require the optional JCS dependency feature; the commands below explicitly run
them after enabling jcs.
# Run unit tests and public behavior integration tests with default features
# Run this crate's tests with all declared features
# Run documentation examples with default features and with jcs
# Verify examples from an external consumer crate; its dependency enables jcs
# Run the root-crate serde_json version × feature matrix (two versions × eight combinations)
# Run the project checks for this crate
# Check code coverage
License
Copyright (c) 2025 - 2026. Haixing Hu. All rights reserved.
Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.
Contributing
Contributions are welcome. Please follow the Rust API guidelines, keep public
API documentation and tests current, and run ./.infra/bin/align-ci.sh to format code and
./.infra/bin/ci-check.sh to satisfy CI requirements before submitting a pull request.
Author
Haixing Hu - Qubit Co. Ltd.
Repository: https://github.com/qubit-ltd/rs-json