blazingly-json 0.1.0

A focused, Tokio-free JSON engine for small protocol and API payloads
Documentation

blazingly-json

CI

Focused, safe, Tokio-free JSON for MCP, JSON-RPC, API, config, snapshot, and JSONL workloads.

blazingly-json is not an incomplete reimplementation of every serde_json feature. Its supported surface was derived from real call sites in mcport, Weavatrix Rust, RadioChron, Blazingly, and BlazingAPI, then tested differentially against serde_json.

The general compatible parser is only modestly faster. The large gain comes from changing protocol architecture: borrow the envelope, avoid a mutable DOM, serialize responses directly, and use an exact schema-aware recognizer where the producer emits a canonical layout.

Current status: 0.1.0 release candidate. The crate passes differential/property tests, Rust 1.78, strict Clippy, rustdoc, packaging, Linux CI, and Windows CI. No production consumer has been switched yet.

Performance summary

Local Windows measurements on an Intel Core Ultra 7 255U. Paired harnesses alternate engine order and report medians. These are workload measurements, not universal parser claims.

Canonical typed MCP call

A generated compact tools/call recognizer borrows three strings, parses a u64 and a boolean, verifies complete consumption, and falls back to the strict order-independent parser on any mismatch.

Path Median range Speedup over typed serde_json
CanonicalBytesScanner over ASCII canonical bytes 45.94-50.47 ns 7.35-7.62x
CanonicalScanner over prevalidated &str 47.63-51.81 ns 7.03-7.43x
strict order-independent JsonCursor 261.82-284.37 ns 1.31-1.35x
typed serde_json derive 342.85-384.78 ns 1.00x

Both canonical paths allocate zero bytes. The byte scanner avoids a complete UTF-8 pre-pass: it matches structural ASCII directly and validates captured ASCII strings while scanning. Across three paired runs it is slightly faster than the already-prevalidated &str path, so byte input no longer pays the previous 10-26% conversion penalty.

This path intentionally accepts only its exact schema, field order, compact separators, and plain strings. The fastest byte method plain_ascii_string rejects non-ASCII fields. Whitespace, reordered fields, escaped or non-ASCII strings, and another schema are not treated as errors: they take the strict JsonCursor fallback.

MCP runtime architecture

Borrowed request routing plus direct response serialization compared with mcport's original owned-Value + clone implementation:

Request Borrowed path Original mcport Speedup
ping 344.93 ns 1,425.04 ns 4.13x
initialize 1,014.87 ns 6,683.85 ns 6.59x
tools/list 2,524.99 ns 7,987.72 ns 3.16x
tools/call 2,103.43 ns 7,298.68 ns 3.47x

Allocation measurements for request dispatch:

Request Borrowed path Original mcport
ping 0 allocations / 0 bytes 8 allocations / 670 bytes
tools/call with owned arguments 5 allocations / 668 bytes 27 allocations / 2,702 bytes
canonical typed tools/call 0 allocations / 0 bytes 27 allocations / 2,702 bytes

RawValue

blazingly_json::value::RawValue implements the common serde_json::value::RawValue contract: borrowed and boxed deserialization, verbatim serialization, from_string, from_string_unchecked, get, into_string, to_raw_value, constants, cloning, and direct deserialization from &RawValue.

Three paired local runs after warm-up produced these ranges:

Workload Speedup over serde_json::RawValue
small MCP borrowed parse 1.22-1.59x
small MCP boxed parse 1.21-1.59x
small MCP from_string 1.34-1.54x
small MCP verbatim to_vec 1.69-2.36x
1,000,000-number borrowed parse, 6.57 MiB 1.42-1.79x
1,000,000-number boxed parse 1.19-1.55x
compact protocol-record array borrowed parse 1.62-2.07x
large preallocated writer output approximately 1.00x

Large output is memory-bandwidth-bound and is reported as parity, not as a multiple-times win. For a complete raw document, RawValue::to_vec and RawValue::write_to bypass the generic Serde marker path. Embedded raw values remain compatible with ordinary to_vec, to_writer, and pretty output.

Allocation measurements:

Operation blazingly-json serde_json
small borrowed parse 0 1 allocation / 8 bytes
small boxed parse 1 allocation / 154 bytes 2 allocations / 162 bytes
from_string with reusable capacity 0 1 allocation / 8 bytes
1,000,000-number borrowed parse 0 0
same payload as owned Vec<u64> in serde_json - 1 allocation + 18 reallocations / 8 MiB live

The raw parser first tries strict compact numeric-array and flat protocol-record recognizers. They must consume a complete value; whitespace, escaping, nesting, or any shape mismatch takes the fully validating general fallback.

Compatible API and large payloads

The compatible owned/typed path does not reach a multiple-times speedup:

Workload blazingly-json serde_json Difference
MCP mutable Value parse 2.195 us 2.313 us +5.37%
MCP typed parse 1.391 us 1.423 us +2.33%
MCP typed encode 420.75 ns 427.20 ns +1.53%
Weavatrix-like graph parse 77.72 us 80.00 us +2.92%
Weavatrix-like graph encode 22.13 us 24.70 us +11.64%
Cargo artifact JSONL parse 1.650 us 1.850 us +12.13%
1,000,000 u64 values 254.9 MiB/s 249.8 MiB/s +2.01%
1,000,000 typed records, 80.98 MiB 150.1 MiB/s 147.6 MiB/s +1.70%
1,000,000 varied MCP Value parses 69.7 MiB/s 68.0 MiB/s +2.48%

Parsing one million owned records makes exactly 2,000,001 allocations and retains 100.27 MiB with either engine: the owned strings and vectors dominate, not the parser. Minimal stripped Windows executables are also effectively tied: 990,208 bytes for blazingly-json and 989,184 bytes for serde_json.

The conclusion is deliberate: replacing serde_json with another compatible owned DOM cannot honestly promise 2-3x less memory or 2-3x more speed. The canonical and borrowed APIs exist to remove that ownership work.

Usage

Until the first crates.io release:

[dependencies]
blazingly-json = { git = "https://github.com/sergii-ziborov/blazingly-json" }
serde = { version = "1", features = ["derive"] }

After publication, the Git dependency becomes:

blazingly-json = "0.1"

Serde-compatible typed JSON

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct Request<'a> {
    method: &'a str,
    limit: u64,
}

let request: Request<'_> =
    blazingly_json::from_str(r#"{"method":"search","limit":20}"#)?;
let encoded = blazingly_json::to_string(&request)?;
# Ok::<(), blazingly_json::Error>(())

Zero-copy envelope

RawJson validates a nested value and borrows its exact input without building a DOM. The payload is materialized only if a selected handler needs it.

use blazingly_json::{RawJson, Value};
use serde::Deserialize;

#[derive(Deserialize)]
struct Call<'a> {
    method: &'a str,
    #[serde(borrow)]
    arguments: RawJson<'a>,
}

let call: Call<'_> = blazingly_json::from_str(
    r#"{"method":"query_graph","arguments":{"limit":20}}"#,
)?;
let arguments = call.arguments.deserialize::<Value>()?;
# Ok::<(), blazingly_json::Error>(())

Drop-in-style RawValue

The type is available both at blazingly_json::value::RawValue and at the crate root.

use blazingly_json::value::{to_raw_value, RawValue};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct Input<'a> {
    #[serde(borrow)]
    payload: &'a RawValue,
}

#[derive(Serialize)]
struct Output<'a> {
    payload: &'a RawValue,
}

let input: Input<'_> =
    blazingly_json::from_str(r#"{"payload": { "limit": 20 }}"#)?;
assert_eq!(input.payload.get(), r#"{ "limit": 20 }"#);

let output = blazingly_json::to_string(&Output {
    payload: input.payload,
})?;
assert_eq!(output, r#"{"payload":{ "limit": 20 }}"#);

let owned = to_raw_value(&[1, 2, 3])?;
assert_eq!(owned.get(), "[1,2,3]");
# Ok::<(), blazingly_json::Error>(())

Order-independent protocol cursor

JsonCursor visits only routing fields, skips unknown values safely, and validates the complete document.

use blazingly_json::JsonCursor;

let mut method = None;
let mut cursor = JsonCursor::from_str(r#"{"jsonrpc":"2.0","method":"ping"}"#);
cursor.object(|request| {
    while let Some(field) = request.next_field()? {
        match field.name() {
            "method" => method = Some(field.string()?),
            _ => field.skip()?,
        }
    }
    Ok(())
})?;
cursor.end()?;
assert_eq!(method.as_deref(), Some("ping"));
# Ok::<(), blazingly_json::Error>(())

Schema-aware canonical path

Use this only as a recognizer with a mandatory fallback. A successful recognizer must consume the complete input.

use blazingly_json::CanonicalScanner;

let input = r#"{"method":"search","limit":20}"#;
let mut scanner = CanonicalScanner::new(input);
let recognized = (|| {
    scanner.literal(r#"{"method":"#)?;
    let method = scanner.plain_string()?;
    scanner.literal(r#","limit":"#)?;
    let limit = scanner.unsigned()?;
    scanner.literal("}")?;
    scanner.is_finished().then_some((method, limit))
})();

if let Some((method, limit)) = recognized {
    assert_eq!((method, limit), ("search", 20));
} else {
    // Parse with JsonCursor or from_str: the input may still be valid JSON.
}

Supported surface

  • Value, Map<String, Value>, Number, and json!;
  • from_str, from_slice, from_value;
  • to_string, pretty/string/vector/writer variants, and to_value;
  • Serde structs, maps, sequences, options, enums, newtypes, and bytes;
  • string, boolean, signed, unsigned, float, null, array, and object values;
  • access, mutation, indexing, JSON Pointer, and take;
  • validated borrowed RawJson;
  • drop-in-style borrowed/boxed RawValue and to_raw_value;
  • routing-oriented JsonCursor;
  • exact-layout CanonicalScanner and CanonicalBytesScanner;
  • strict RFC 8259 parsing with recursion limits and source locations.

Deliberate 0.1 exclusions:

  • arbitrary-precision numbers;
  • JSON5, comments, trailing commas, NaN, and infinities;
  • preserve_order;
  • synchronous Read-based incremental parsing;
  • private serde_json implementation details other than the established RawValue Serde token used for serializer interoperability.

Dependencies and safety

The runtime dependencies are serde, memchr, itoa, lexical-core, and zmij. serde_json is a development-only oracle for differential tests and benchmarks. Tokio, Hyper, and Axum are absent.

unsafe_code is denied crate-wide. The sole exception is raw_value.rs, which contains three documented repr(transparent) DST conversions between str and RawValue. No parser, scanner, serializer, or other module may use unsafe code. The optional from_string_unchecked API is itself unsafe and states the same validity invariant as the serde_json API it replaces.

Competitive position

  • serde_json: broad compatibility, maturity, ecosystem, and an excellent borrowed RawValue;
  • sonic-rs: SIMD parsing and lazy field access;
  • simd-json: SIMD tape plus borrowed and owned DOMs, with mutable-input and unsafe-code trade-offs;
  • serde_json_borrow: borrowed DOM and reduced string allocation;
  • jiter: iterator/schema-oriented parsing plus Serde support.

blazingly-json does not claim to beat every engine on every document. Its target is portable, low-MSRV protocol performance, a tiny auditable unsafe island for RawValue, and generated schema-aware MCP/API codecs with a strict fallback.

See:

Verification

cargo fmt --all -- --check
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo +1.78 check --lib
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps
cargo package

cargo bench --bench paired_comparison
cargo bench --bench large_payload
cargo bench --bench allocation_comparison
cargo bench --bench mcp_fast_path
cargo bench --bench mcp_allocations
cargo bench --bench mcport_end_to_end
cargo bench --bench raw_value_comparison
cargo bench --bench raw_value_allocations

License

MIT