Wado

Serialization

core:serde is a format-agnostic serialization framework. This chapter covers its traits, the shape a derived impl writes each type in, and the formats the standard library provides. One pair of traits serves every format:

pub trait Serialize with () {
    fn serialize<S: Serializer>(&self, s: &mut S) -> Result<(), SerializeError>;
}

pub trait Deserialize with () {
    fn deserialize<D: Deserializer>(d: &mut D) -> Result<Self, DeserializeError>;
}

test {
    assert json::to_string(&Reading { c: Celsius { deg: 21 } }).unwrap() == "21";
    assert json::from_string::<Reading>("21").unwrap().c.deg == 21;
}

A format implements Serializer, Deserializer, or both, and a value's Serialize / Deserialize impl works with any of them. The formats in the standard library are core:json and core:cbor, which do both, and core:args, which only deserializes. core:serde lists each trait's methods.

Deserialize::deserialize has no self: it builds a new value from what the deserializer reads. Typed deserialization is pulled by the value's impl. A format's deserialize_any hands the input to a Visitor instead, for input whose shape is not known ahead.

The data model a Serializer accepts is:

Failure is one of two fixed error types, whichever direction failed. SerializeError carries a kind (UnsupportedValue, Custom) and a message. DeserializeError carries a kind (UnexpectedType, MissingField, UnknownVariant, DuplicateField, InvalidValue, Overflow, MalformedInput, TrailingData, Eof, DepthLimitExceeded, Custom), a message, and the byte offset into the input, or -1 where the failure has no position. Nesting deeper than a format's max_depth is a DepthLimitExceeded error, not a trap.

A format is self-describing (SD) when the input names what it holds, such as core:json writing a struct as an object keyed by field name. A self-describing format resolves each key to a field by its wire name. A non-self-describing (NSD) format leaves the reader to know what the input holds, as core:args does. The same Deserialize impl reads both.

Rationale: WEP: Serialization and Deserialization.

Serialization Names

A struct field, enum case, variant case, or flags member goes on the wire under its name in the source, unchanged: user_name stays user_name and Red stays Red. This is its wire name. Wire Names states how #[wire(...)] renames one.

Serialized Shapes

A derived impl writes each kind of type in one shape:

Type Written as
struct a struct with its type name and each field's wire name and value, in declaration order
enum a unit variant case: the live case's wire name and discriminant
variant a unit case as enum writes one; a payload case with its one payload
flags a sequence of the set members' wire names, in declaration order
tuple a sequence of its elements

Reading back is the mirror. A variant is read by its discriminant where the format reports one, as an NSD format does, and by its wire name otherwise.

A variant is externally tagged, as in Rust's serde, and no other representation exists. In JSON:

Value JSON
Color::Red "Red"
Shape::Circle(5.0) {"Circle": 5.0}
Shape::Rectangle([10.0, 20.0]) {"Rectangle": [10.0, 20.0]}

The standard library's own impls:

Type Written as
integers, floats, bool, char the matching data model entry
String a string
ByteList, ByteArray, ByteSlice a byte string
() null
Option<T> null for None; the held value for Some
Result<T, E> a variant with cases Ok and Err
List<T> a sequence
TreeMap<K, V> a map
TreeSet<T> a sequence

A text format spells every map key as a string, so a key there must be a scalar: an integer, a float, bool, char or a string. A non-string key is written as the string of its spelling ({"1": …} for the integer 1) and parsed back from it. Any other key type is an UnsupportedValue error on writing and an UnexpectedType error on reading. A binary format with keys of its own, such as core:cbor, writes the key as the value it is.

A format with no byte string of its own writes one as a sequence of u8. () and None both write null, so Option<()> carries no information: Some(()) reads back as None. A () anywhere else round-trips. Likewise Some(None) and None of an Option<Option<T>> are both null in JSON.

Missing, Repeated, and Unknown Fields

A missing field is a MissingField error unless the field declares a default (f: T = expr, see Struct Field Defaults). A defaulted field is optional and falls back to its default when absent. This is the only mechanism for an optional field, and no type is special-cased. A bare Option<T> field is required: a self-describing format needs its key present, though the value may be null. Option<T> = null is the optional one. For a zero-value fallback, write the zero literal (= 0, = "", = false, = [], = null).

struct Config {
    host: String,            // required: MissingField if absent
    port: i32 = 8080,        // optional: 8080 if absent
    tags: List<String> = [], // optional: [] if absent
}

test {
    let c = from_string::<Config>(`{"host":"h"}`).unwrap();
    assert c.port == 8080 && c.tags == [];
    assert from_string::<Config>(`{"port":1}`).unwrap_err().kind matches { MissingField };
}

#[wire(default)] does not exist. Writing it is a compile error that points to a field default instead.

Deserialization rejects a repeated field or key by default. A set's element is its key, so a TreeSet<T> read from a sequence that repeats an element is rejected the same way. A format that overrides Deserializer::on_duplicate_field may accept one instead. An accepted repeat keeps the last value, and a map key or set element as first read.

A self-describing format skips a key that names no field, whatever value it holds.

Bound-Driven Serialize / Deserialize

Serialize and Deserialize derive on demand, as Derivation Policy states, so a type needs no marker to be serializable. An anonymous struct, which no marker can name, is serializable the same way:

use { to_string } from "core:json";

struct Point { x: i32, y: i32 }              // no impl marker needed

test {
    let json = to_string(&Point { x: 1, y: 2 });
    assert json.unwrap() == `{"x":1,"y":2}`;
    let anon = to_string(&{ x: 1, y: 2 });       // the same JSON, from an anonymous struct
    assert anon.unwrap() == json.unwrap();
}

The marker impl Serialize for T; (Compiler-Synthesized impl) forces the impl where no bound asks for it. #[wire(...)] attributes apply with or without it.

Deriving on demand means a type becomes serializable the moment some code asks, and a field added later extends its wire shape. A hand-written impl is the control where that matters.

JSON Module (core:json)

use { to_string, from_string } from "core:json";

struct User { name: String, age: i32 }

test {
    let user = User { name: "Alice", age: 30 };

    // Serialize to JSON string
    let json = to_string::<User>(&user).unwrap();
    assert json == `{"name":"Alice","age":30}`;

    // Deserialize from JSON string
    let back = from_string::<User>(json).unwrap();
    assert back.name == "Alice" && back.age == 30;
}

Serializing a NaN or an infinite float is an Err. Deserializing malformed input, a type mismatch, or input with trailing data is an Err. So is a number that rounds to infinity in the float type it is read as (1e400 as f64), an Overflow. One too small for the type reads as a zero of its sign.

core:json writes i64 and u64 as a JSON number while the magnitude is at most 2^53 - 1, the largest integer a JavaScript number holds exactly, and as a JSON string of decimal digits beyond it. It writes i128 and u128 as such a string whatever the value. Reading any number type, integer or float, accepts a string holding exactly one number token as well as the bare number.

Command-Line Arguments (core:args)

core:args is a non-self-describing, parse-only Deserializer over argv. An argument type is an ordinary struct. Each field is a --long option, unless #[wire(positional)] marks it as a positional.

use { parse } from "core:args";

struct Cli {
    #[wire(positional)] input: String,
    jobs: i32 = 1,
    verbose: bool = false,
}

test {
    let cli = parse::<Cli>(["in.txt", "--jobs", "4", "--verbose"]).unwrap();
    assert cli.input == "in.txt" && cli.jobs == 4 && cli.verbose;
}

parse::<T>(argv) takes the arguments as a List<String> and performs no effect, so a test or a dispatcher passes them in. from_env::<T>() reads them from core:cli::args() and requires Environment. Neither sees the program name. Both return Result<T, ArgsError>, whose kind is one of UnknownOption, MissingValue, MissingArgument, UnknownSubcommand, InvalidValue, and TooManyPositionals.

Options

Only long options exist: --name value, --name=value, and a bare --name for a bool flag. There are no short options, no bundling, and no prefix abbreviation. - and _ fold in an option name, so --dry-run binds dry_run. A value never starts with --, so --name --next is MissingValue rather than a value, while a single - starts one (--delta -5). After a bare --, every token is positional.

A field's type and whether it declares a default decide its arity:

Field On the command line
T required --name <value>
T = expr optional --name <value>; absent is expr
Option<T> = null optional --name <value>; absent is null
bool = false flag --name; absent is false
List<T> = [] repeatable, zero or more times
List<T> repeatable, at least once

A repeated option may be interspersed with others, and always takes a value. A struct whose every field has a default parses an empty argv without error.

A token converts to a scalar with LenientFromStr, so --jobs 0x10, --retries 1_000, and --verbose=1 parse. A failed conversion is InvalidValue. An enum-typed option takes a case's wire name as its value, and a value naming no case is InvalidValue.

Positionals and Subcommands

A #[wire(positional)] field is filled from bare tokens, in declaration order and never by --name. A defaulted positional is optional, and a List<T> = [] positional takes every remaining bare token. A bare token beyond the last positional is TooManyPositionals.

A subcommand set is a positional field whose type is a variant. Its tag is the leading bare token, matched against the case's wire name, and the case's payload parses the tokens after it. A tag naming no case is UnknownSubcommand. #[wire(name_policy = "kebab-case")] on the variant makes AddRemote the tag add-remote; no case folding happens beyond the wire name.

struct AddArgs { #[wire(positional)] path: String, all: bool = false }

#[wire(name_policy = "kebab-case")]
variant Command {
    Add(AddArgs),          // tag `add`
    Remote(RemoteCmd),     // nested: the payload holds another variant
}

#[wire(name_policy = "kebab-case")]
variant RemoteCmd { List }

struct Cli {
    verbose: bool = false,
    #[wire(positional)] command: Command,
}

test {
    let cli = parse::<Cli>(["--verbose", "add", "--all", "x"]).unwrap();
    assert cli.verbose && cli.command matches { Add(a) && a.all && a.path == "x" };
    assert parse::<Cli>(["remote", "list"]).unwrap().command matches { Remote(List) };
}

Options before the tag bind to the outer struct and options after it to the subcommand, so prog --verbose add --all x works. Nesting needs nothing more.

Positionals bind greedily in declaration order, so a well-formed struct puts required positionals before optional ones, at most one variadic positional, and that one last, and never a variadic positional beside a subcommand. A struct that breaks this is an error where it is declared.

Rationale: WEP: Command-Line Argument Parsing.