# Qubit JSON
[](https://github.com/qubit-ltd/rs-json/actions/workflows/ci.yml)
[](https://qubit-ltd.github.io/rs-json/coverage/)
[](https://crates.io/crates/qubit-json)
[](https://www.rust-lang.org)
[](LICENSE)
[](README.zh_CN.md)
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.11` version described by this checkout:
```toml
[dependencies]
qubit-json = "0.11"
qubit-budget = { version = "0.9", features = ["json"] }
serde_json = "1.0"
```
Local checkout:
```toml
[dependencies]
qubit-json = { version = "0.11", path = "../rs-json" }
qubit-budget = { version = "0.9", features = ["json"] }
serde_json = "1.0"
```
Add `serde = { version = "1.0", features = ["derive"] }` when decoding into
derived application types.
To enable RFC 8785 canonical output, opt into the feature explicitly:
```toml
qubit-json = { version = "0.11", features = ["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:
```rust
use qubit_budget::json::JsonDecodeLimits;
use qubit_budget::json::JsonResource;
use qubit_json::decode::JsonDecodeError;
use qubit_json::decode::JsonDecodeErrorKind;
use qubit_json::decode::JsonDecoder;
fn main() -> Result<(), JsonDecodeError<JsonResource>> {
let limits = JsonDecodeLimits::<qubit_budget::json::JsonResource, usize>::builder()
.max_input_bytes(4096)
.max_depth(32)
.max_nodes(256)
.max_sequence_items(64)
.max_map_entries(64)
.max_key_bytes(128)
.max_string_bytes(2048)
.max_number_bytes(20)
.max_payload_bytes(4096)
.build();
let mut decoder = JsonDecoder::with_limits(limits);
let value: serde_json::Value =
decoder.decode_utf8(br#"{"id":18446744073709551615,"ok":true}"#)?;
assert_eq!(value["id"], serde_json::json!(u64::MAX));
let small_limits = limits.into_builder().max_input_bytes(8).build();
let mut small_decoder = JsonDecoder::with_limits(small_limits);
let error = small_decoder
.decode_utf8::<serde_json::Value>(br#"{"ok":true}"#)
.expect_err("the request body must exceed eight bytes");
assert_eq!(error.kind(), JsonDecodeErrorKind::Budget);
Ok(())
}
```
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](../rs-web/examples/controller_crud.rs)
and the [user guide's `decode_input` section](doc/user_guide.md#advanced-usage).
## Choose the decoding boundary
Choose the entry point from the input contract, not from whether the payload
happens to parse:
| Input must already be complete, strict JSON | `JsonDecoder` |
| 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 `NormalizedJsonDocument` decoding methods |
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`:
```rust
use qubit_budget::json::{JsonEncodeLimits, JsonResource};
use qubit_json::encode::JcsEncoder;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let limits = JsonEncodeLimits::<JsonResource, usize>::builder()
.max_output_bytes(4096)
.build();
let mut encoder = JcsEncoder::with_limits(limits, 16 * 1024);
let value = serde_json::json!({"b": 2, "a": 1.0});
assert_eq!(encoder.to_vec(&value)?, br#"{"a":1,"b":2}"#);
Ok(())
}
```
The `jcs` feature is disabled by default. JCS canonicalizes the `Value` model;
it does not accept arbitrary `Serialize` values or raw JSON text. For raw input,
reject duplicate object keys before constructing a `Value`, since duplicates
cannot be recovered afterward. 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.
## 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
| `decode` | Strict JSON admission or explicitly configured text normalization | Normalization applies only configured transformations; it never invents missing JSON syntax |
| `encode` | Budgeted strict JSON output | Value accounting commits after complete serialization; an I/O failure can still leave bytes in an external writer |
| `value` | Building budgeted `serde_json::Value` trees from Serde events | A 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; visitor and output-budget failures do not roll back prior changes |
`qubit-budget` owns limits, resource identities, budgets, and sessions.
`qubit-json` owns JSON normalization, lexical validation, text codecs, value
construction, and traversal.
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
- 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` when uniqueness is part of the contract.
- Negative integers fit `i64`, non-negative integers fit `u64`, and fractional
or exponential values must be finite `f64`. Use strings or domain types for
wider integers or exact decimals that must avoid binary rounding.
- 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 complete `&str` or
`&[u8]` inputs; use them when the decoded result must borrow from the input.
`JsonDecoder::decode_input` instead reads an `Input<Item = u8>` source through
EOF and returns an owned `DeserializeOwned` value. It buffers the complete
document; without `max_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.
- `JcsEncoder::write_buffered` and `write_incremental` can 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_bytes` bounds auxiliary traversal and object-sorting capacity,
not the input `Value`, allocator metadata, or output buffer capacity.
- 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. Enable `DiagnosticPolicy::Detailed` only
where input-derived sources are safe to retain and log.
## Learn more
- [English user guide](doc/user_guide.md) ·
[中文用户手册](doc/user_guide.zh_CN.md)
- [JSON number contract](doc/number_contract.md) ·
[JSON 数字契约](doc/number_contract.zh_CN.md)
- [Design documents](doc/json_design.md) ·
[设计文档](doc/json_design.zh_CN.md)
- [Benchmark guide](doc/benchmark_baseline.md) ·
[基准测试指南](doc/benchmark_baseline.zh_CN.md)
- [Released API documentation](https://docs.rs/qubit-json); for the current
branch API, run `cargo doc --all-features --open`
## Testing
```bash
# Run tests with the default feature set
cargo test
# Run tests with all declared features
cargo test --all-features
# Run the standalone Markdown documentation examples
cargo test --manifest-path tests/fixtures/documentation_examples/Cargo.toml --doc --locked
# Project CI checks
./.infra/bin/ci-check.sh
# Check code coverage
./.infra/bin/coverage.sh
```
## License
Copyright (c) 2025 - 2026. Haixing Hu. All rights reserved.
Licensed under the Apache License, Version 2.0. See [LICENSE](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](https://github.com/qubit-ltd/rs-json)