nanojson
nanojson is a zero-dependency, no-std compatible JSON serializer and pull-parser with hand-written derives (no serde, no pro-macro2). It uses an immediate-mode API, validating your schema while parsing.
use ;
// Annotate your types once:
// std API
let json: String = stringify?;
let point: Point = parse?;
// no-std API
let mut buf = ; // <-- json string stored here
let json: &str = stringify_sized?;
let point: Point = parse_sized?; // <- provide scratch buffer
Included blanket implementations, per feature:
no-std: primitive types includingf32/f64, fixed sized arrays,Option<T>- optional support for:
alloc:String,Vec<T>,Box<T>,BTreeMap<String, V>std:HashMap<String, V>deriveDerive macros#[derive(Serialize, Deserialize)]arrayvec:ArrayVec<T, N>andArrayString<N>
By default, alloc, std and derive features are enabled
Derive macros
Use derive feature (enabled by default) and annotate your types:
use ;
Two API tiers
std tier (default feature)
No buffer choices. The output String grows as needed; the scratch buffer for parsing is auto-allocated to src.len() bytes (a safe upper bound).
Serialization
// One-liner for a derived type
let json: String = stringify?;
// Closure form for hand-written JSON
let json: String = stringify_as?;
Deserialization
// One-liner for a derived type
let entity: Entity = parse?;
// Closure form for manual parsing
let json = r#"{"x": 3, "y": 4}"#;
let = parse_as?;
no_std tier
All memory on the stack. You choose output buffer size when stringifying, and scratch buffer size for parsing (scratch buffer only needs to fit the longest single field value after escape-decoding, typically 32–128 bytes).
Serialization
// One-liner for a derived type
let mut buf = ;
let json = stringify_sized?;
// Closure form
let json = stringify_sized_as?;
Deserialization
// One-liner for a derived type (STR_BUF = 64)
let entity: Entity = parse_sized?;
// Low-level parser for hand-written code
let json = r#"{"x": 3, "y": 4}"#.as_bytes;
let = parse_sized_as?;
Size estimation
let n = measure;
// n is the exact byte count, use it to pick N in stringify_sized / parse_sized.
Pretty-printing
Pass an indent width to the _pretty or _compact variants of any serialization function:
// std tier
let json = stringify_pretty?; // indent level
let json = stringify_pretty_as?;
let json = stringify_compact?; // line len + indent
let json = stringify_compact_as?;
// no_std tier
let mut buf = ;
let json = stringify_sized_pretty?;
let json = stringify_sized_pretty_as?;
Pretty
Compact: (max line length 50)
Error handling
Parse errors are ParseError { kind: ParseErrorKind, offset: usize }.
The offset is a byte position in the source slice.
| Kind | Meaning |
|---|---|
UnexpectedToken { expected, got } |
Parser expected one token type, found another |
UnexpectedEof |
Input ended before the value was complete |
InvalidEscape(byte) |
Unknown \X escape sequence |
StringBufferOverflow |
Decoded string didn't fit in the scratch buffer |
InvalidUtf8 |
String content is not valid UTF-8 after unescaping |
UnknownField { type_name, expected_fields } |
Key not recognised by the deserializer |
MissingField { field } |
A required field was absent (used by derived code) |
Parse error can be printed with a nice looking error message like so:
match
27 | "not_a_valid_field": {},
| ^
| unknown field in `Inventory`, expected one of: `items`, `metadata`
Serialization errors are SerializeError<W::Error>:
| Variant | Meaning |
|---|---|
Write(e) |
Write error from the sink (e.g. WriteError::BufferFull from SliceWriter) |
DepthExceeded |
Nesting exceeded the DEPTH const generic (default 32) |
InvalidState |
member called outside an object or twice without an intervening value |
InvalidUtf8(offset) |
Final string isn't utf-8 compatible. This indicates a serialization bug |
Workspace layout
nanojson/
├── Cargo.toml workspace root
├── nanojson/ core library (#![no_std], no dependencies)
│ ├── src/
│ │ ├── lib.rs
│ │ ├── write.rs Write, SliceWriter, SizeCounter
│ │ ├── serialize.rs Serializer, SerializeError, Serialize
│ │ ├── deserialize.rs Parser, Deserialize
│ │ └── error.rs Error types
│ ├── examples/
│ └── tests/
└── nanojson-derive/ proc-macro crate (no syn/quote/proc_macro2)
Core concepts
| Concept | What it is |
|---|---|
Serializer<W> |
Serializer. You call methods (object_begin, member, integer, …) in order and it writes JSON to your W: Write sink. |
Parser<'src, 'buf> |
Pull parser. You drive it step by step (object_begin, member, integer, …). It never builds a tree. |
Write |
Trait for output sinks. SliceWriter writes into a &mut [u8]. SizeCounter counts bytes without writing (useful for pre-sizing). Vec<u8> implements Write when the std feature is on. |
Serialize |
Trait implemented by types that know how to write themselves. Primitive impls are provided. |
Deserialize |
Trait implemented by types that know how to parse themselves. |
Limitations
- Scratch buffer is reused per string.
Parser::string()andParser::member()both write into the same&mut [u8]. The returned&stris invalidated by the next string parse. Copy the value immediately if you need to keep it. - No streaming / async. The serializer writes synchronously to
Write; the parser requires the entire input to be in memory at once. - No
serdecompatibility. nanojson is its own trait ecosystem. If you need serde interop, use serde. - Non-finite floats are an error. Serializing
f32::NAN,f64::INFINITY, etc. returnsSerializeError::InvalidValue. JSON has no representation for these values. - Nesting depth limit. The serializer's
DEPTHconst generic (default 32) limits how deeply you can nest objects and arrays. UseSerializer<W, 64>directly for deeper structures. - No multi member tuple enum variants
- Use
enum Kind { A { number: i32, name: String } }instead ofenum Kind { A(i32, String) }
- Use
Running the examples
Running the tests