Skip to content

Latest commit

 

History

594 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

deser

deser: an experimental serialization and deserialization library for Rust

Crates.io License Documentation

Deser is a serialization library for Rust for self describing formats such as JSON, YAML, TOML, INI, CBOR, MessagePack, XML, property lists, PHP's serialize, Python's pickle, 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 deser::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
#[deser(rename_all = "camelCase")]
pub struct Account {
    id: u64,
    account_holder: String,
    #[deser(default)]
    is_deactivated: bool,
}

let json = r#"{"id": 42, "accountHolder": "Jane"}"#;
let account: Account = deser_json::from_str(json).unwrap();
assert_eq!(account.account_holder, "Jane");
assert_eq!(
    deser_json::to_string(&account).unwrap(),
    r#"{"id":42,"accountHolder":"Jane","isDeactivated":false}"#
);

Deriving requires the derive feature, which is not enabled by default:

cargo add deser --features derive
cargo add deser-json

Crates

The same type works unchanged with every format (CSV as long as it's flat).

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 Send and 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. Their category tells malformed input apart from input that does not fit your types (HTTP 400 vs. 422).
  • Layers sit between the format and your types and can track paths, rename keys or reject input.
  • Safe defaults for untrusted input: duplicate keys are an error by default, limits for depth, size and length are configured once in a context.

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 std::net::IpAddr;
use deser::{Deserialize, Serialize};
use deser::adapters::DisplayFromStr;
use deser::de::Recording;
use deser_validate::{Check, validator};

mod keys {
    pub const KIND: &str = "@type";
}

validator!(NonZero(port: &u16) => *port != 0, "must not be zero");

#[derive(Debug, Serialize, Deserialize)]
#[deser(tag = keys::KIND, tag_alias = "type", rename_all = "snake_case")]
pub enum Listener {
    // picked if the tag is missing
    #[deser(default)]
    Tcp {
        #[deser(as = DisplayFromStr)]
        host: IpAddr,
        #[deser(default = 8080, as = Check<NonZero>)]
        port: u16,
    },
    Unix { path: String },
    // kinds this version does not know are kept and written back
    #[deser(other)]
    Other(#[deser(tag)] String, Recording),
}

let listener: Listener =
    deser_json::from_str(r#"{"host": "127.0.0.1"}"#).unwrap();
assert!(matches!(listener, Listener::Tcp { port: 8080, .. }));

let input = r#"{"@type":"quic","host":"::1","alpn":["h3"]}"#;
let listener: Listener = deser_json::from_str(input).unwrap();
assert_eq!(deser_json::to_string(&listener).unwrap(), input);

let input = r#"{"host": "::1", "port": 0}"#;
let err = deser_json::from_str::<Listener>(input).unwrap_err();
assert_eq!(
    err.to_string(),
    "InvalidValue: invalid value: must not be zero at line 1 column 25"
);

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 deser::Deserialize;
use deser_path::{Path, PathLayer};

#[derive(Debug, Deserialize)]
struct Config {
    servers: Vec<Server>,
}

#[derive(Debug, Deserialize)]
#[deser(tag = "type", rename_all = "lowercase")]
enum Server {
    Http { url: String, timeout: u32 },
    File { path: String },
}

let toml = r#"
[[servers]]
type = "file"
path = "/srv/www"

[[servers]]
url = "https://example.com/"
timeout = "30s"
type = "http"
"#;

let err = deser_toml::Deserializer::from_str(toml)
    .deserialize_with::<Config, _>(|driver| {
        driver.push_layer(PathLayer::new())
    })
    .unwrap_err();
let path = err.attachment::<Path>().unwrap();
assert_eq!(path.to_string(), "servers[1].timeout");
assert_eq!((err.line(), err.column()), (Some(8), Some(11)));

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 in an arena instead of on the call stack, 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.

License and Links

About

Experimental rust serialization library

Topics

Resources

Stars

443 stars

Watchers

7 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages