Deser is a serialization library for Rust for self describing formats such as
JSON, YAML, TOML, CBOR, MessagePack, XML, property lists, CSV and query
strings. It takes the user experience of serde, the problems that years of
running serde in production turned up and the Rust of today, and tries to
solve them with a different architecture. If you know serde you will feel at
home: you derive Serialize and Deserialize, pick a format crate, and most
attributes have the names you already know.
use ;
let json = r#"{"id": 42, "accountHolder": "Jane"}"#;
let account: Account = from_str.unwrap;
assert_eq!;
assert_eq!;
Deriving requires the derive feature, which is not enabled by default:
Crates
The same type works unchanged with every format (CSV as long as it's flat).
- Core: deser (the crate you depend on),
deser-derive (the
derivefeature) and deser-core (internal, what format crates depend on) - Formats: deser-json, deser-jsonc, deser-json5, deser-hj, deser-yaml, deser-toml, deser-cbor, deser-msgpack, deser-xml, deser-plist (XML, binary and OpenStep), deser-csv (CSV and TSV), deser-urlencoded (query strings and forms), deser-env (environment variables), deser-debug (debug formatting)
- Layers and adapters: deser-path (paths in errors), deser-location (line and column of values), deser-validate (validation), deser-encoding (hex and base32)
- Integrations: deser-value (dynamic values), deser-transcode (converting between formats), deser-tokio (async IO), deser-serde (serde interop)
Why Deser?
Serde is one of the most important crates in the Rust ecosystem and its stability is a big part of why. That same stability also means that some of its problems cannot be fixed without breaking every format and every hand written implementation. Deser is an experiment to see what a serialization system looks like that is allowed to start over:
- Buffering does not lose information. Internally tagged and untagged
enums record values as events together with everything the format knows
about them, so
u128, exact numbers, numeric keys and error locations survive. - Flattening is native and does not buffer at all, also with
deny_unknown_fields. - No stack overflows. Deser does not recurse on the call stack, so deep nesting needs no recursion limit.
- Enums are more capable. Catch-all variants can hold data and round trip, a default variant can be picked if the tag is missing and tags can be integers and booleans.
- Customizations compose. Adapters nest
(
as = Option<Vec<DisplayFromStr>>) and validation is an adapter too. - Attributes are Rust, not strings. Defaults, names, adapters and bounds are expressions and types the compiler checks.
- Ready for async. An ongoing deserialization is
Sendand can be fed while the input arrives. - Errors point at the problem with line, column and the path to the value, also inside buffered values.
- Layers sit between the format and your types and can track paths, enforce limits, rename keys or reject input.
- Safe defaults for untrusted input: duplicate keys are an error by default.
Many of these came up while building Sentry Relay, which processes untrusted JSON at scale. SERDE.md goes through them in detail and links the serde issues they correspond to.
A Taste
A single enum that shows a few things that would need hand written code or extra crates with serde:
use IpAddr;
use ;
use DisplayFromStr;
use Recording;
use ;
validator!;
let listener: Listener =
from_str.unwrap;
assert!;
let input = r#"{"@type":"quic","host":"::1","alpn":["h3"]}"#;
let listener: Listener = from_str.unwrap;
assert_eq!;
let input = r#"{"host": "::1", "port": 0}"#;
let err = .unwrap_err;
assert_eq!;
Errors That Help
Here the tag of an internally tagged enum comes last, so the values have to be buffered until it is known. In serde this is where locations and paths get lost, in deser they are retained:
use Deserialize;
use ;
let toml = r#"
[[servers]]
type = "file"
path = "/srv/www"
[[servers]]
url = "https://example.com/"
timeout = "30s"
type = "http"
"#;
let err = from_str
.
.unwrap_err;
let path = err..unwrap;
assert_eq!;
assert_eq!;
More practical examples are in the examples folder.
How It Works
Instead of visitors calling into each other recursively, deserializing a type creates a sink that receives events and serializing it produces emitters that hand out nested values. Nested sinks and emitters are returned to a driver which keeps them on the heap, which is why nesting cannot overflow the stack and a deserialization can be suspended between events. The data model is small (atoms, maps and sequences) and can be extended with extension atoms that carry a fallback for formats which do not understand them. Where buffering cannot be avoided, events are recorded together with the state the format published for them and replayed as such.
Limitations
Deser only supports self describing formats, so bincode, postcard and similar formats are out of scope. Known limitations, performance numbers and notes on unsafe code are in LIMITATIONS.md.
Inspiration
This crate heavily borrows from
miniserde,
serde and Sentry Relay's meta
system. The general trait design was
modelled after miniserde.