Sign in

    data

    A serde-shaped serialization framework: one data model, many formats

    serde
    serialization
    json
    derive
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    104

    #Yu-zh/data

    A serde-shaped serialization framework for MoonBit.

    A type describes itself in terms of a fixed data model. A format knows how to read and write that data model. Neither knows about the other — so N types and M formats cost N + M implementations instead of N × M.

    ///|
    #data.derive(Serialize, Deserialize)
    pub(all) struct User {
    name : String
    age : Int
    }

    // Works with every format, without the type mentioning any of them.

    ///|
    let bytes = @json.to_bytes(user)

    ///|
    let value = @data.to_value(user)

    #Install

    moon add Yu-zh/data

    Then import what you need in moon.pkg:

    import { "Yu-zh/data", "Yu-zh/data/json", }

    #The five pieces

    Serializea type describing itself to any Serializer
    Deserializea type reconstructing itself from any Deserializer
    Serializera format receiving the data model
    Deserializera format producing the data model
    Valuethe data model made concrete, for when a type is not known ahead of time

    Value holds strings as UTF-8 BytesViews, not as MoonBit Strings. A document contains text in some encoding; turning that into UTF-16 is a service to one particular consumer, so the consumer that wants it pays — Value::text() — and a document can otherwise be read and written again without ever being converted. Parsing borrows: a string in a Value is a view into the document it came from, so no text is copied at all — MoonBit's collector keeps the document alive, with none of the lifetime machinery Rust needs for the same trick. The cost is retention, so Value::detach() deep-copies when a caller keeps a few fields and wants the document released.

    Serializer and Deserializer carry both paths: serialize_string / deserialize_string for text, serialize_str_bytes / deserialize_str_bytes for UTF-8, each defaulting to the other so a format implements only what suits it.

    The two traits a type implements are, in full:

    ///|
    pub(open) trait Serialize {
    fn[S : Serializer] serialize(Self, S) -> Unit raise SerError
    }

    ///|
    pub(open) trait Deserialize {
    fn[D : Deserializer] deserialize(D) -> Self raise DeError
    }

    Both are polymorphic in the format, so neither has a trait-object form — the same as in Rust. Reach for Value when a heterogeneous collection is needed.

    #Writing an implementation

    Everything ships with implementations already: the fourteen primitives (Bool, Int, Int64, UInt, UInt64, Int16, UInt16, Byte, Float, Double, Char, String, Bytes, Unit), plus Option, Array, FixedArray, Map, 2- and 3-tuples, and Value itself.

    For your own types, either write the two implementations or generate them. Written by hand they look like this:

    ///|
    pub(all) struct User {
    name : String
    age : Int
    /// An `Option` field: absent and explicitly null both read back as `None`.
    nickname : String?
    } derive(Eq, Debug)

    ///|
    pub impl @data.Serialize for User with fn serialize(self, s) {
    s.serialize_struct_begin("User", 3)
    s.serialize_field("name", self.name)
    s.serialize_field("age", self.age)
    s.serialize_field("nickname", self.nickname)
    s.serialize_struct_end()
    }

    ///|
    let user_fields : ReadOnlyArray[String] = ["name", "age", "nickname"]

    ///|
    pub impl @data.Deserialize for User with fn deserialize(d) {
    d.deserialize_struct_begin("User", user_fields)
    let mut name = None
    let mut age = None
    let mut nickname = None
    while d.deserialize_field_name() is Some(field) {
    match field {
    "name" => name = Some(d.deserialize_field_value())
    "age" => age = Some(d.deserialize_field_value())
    "nickname" => nickname = d.deserialize_field_value()
    // Unknown fields are skipped rather than rejected.
    _ => d.skip_value()
    }
    }
    {
    name: @data.required(name, name="name", path=d.path()),
    age: @data.required(age, name="age", path=d.path()),
    nickname,
    }
    }

    Note that no field type is ever named: type inference flows backward from the struct literal, so the accumulators need no annotations. The format sees the ordinary immutable field-name array. Derived code additionally generates a private field enum implementing FieldIdentifier; byte-oriented formats can then map raw UTF-8 directly to semantic cases without exposing numeric indices or a lookup-table representation in the public API.

    That one pair of implementations now drives every format:

    ///|
    test "one implementation, every format" {
    let user = User::{ name: "ada", age: 36, nickname: None }

    // JSON, as UTF-8 bytes.
    inspect(
    @json.to_string(user),
    content="{\"name\":\"ada\",\"age\":36,\"nickname\":null}",
    )
    assert_eq((@json.from_string(@json.to_string(user)) : User), user)

    // The same value in memory, with no encoding step.
    inspect(
    @data.to_value(user),
    content="{\"name\": \"ada\", \"age\": 36, \"nickname\": null}",
    )
    assert_eq((@data.from_value(@data.to_value(user)) : User), user)
    }

    #Enums

    Enums use serde's default external tagging: a variant with no payload is a bare string, and anything else is a one-entry object.

    ///|
    pub(all) enum Role {
    Guest
    Member(String)
    Admin(level~ : Int, since~ : String)
    } derive(Eq, Debug)

    ///|
    pub impl @data.Serialize for Role with fn serialize(self, s) {
    let name = "Role"
    match self {
    Guest =>
    s.serialize_unit_variant({ enum_name: name, index: 0, name: "Guest" })
    Member(team) =>
    s.serialize_newtype_variant(
    { enum_name: name, index: 1, name: "Member" },
    team,
    )
    Admin(level~, since~) => {
    s.serialize_struct_variant_begin(
    { enum_name: name, index: 2, name: "Admin" },
    2,
    )
    s.serialize_struct_variant_field("level", level)
    s.serialize_struct_variant_field("since", since)
    s.serialize_struct_variant_end()
    }
    }
    }

    ///|
    test "enums are externally tagged" {
    inspect(@json.to_string(Guest), content="\"Guest\"")
    inspect(@json.to_string(Member("core")), content="{\"Member\":\"core\"}")
    inspect(
    @json.to_string(Admin(level=2, since="2019")),
    content="{\"Admin\":{\"level\":2,\"since\":\"2019\"}}",
    )
    }

    #Deriving instead

    MoonBit has no user-defined derive, so implementations are generated by a tool rather than by the compiler. Mark a type with an attribute — the compiler ignores attributes it does not recognise:

    ///|
    #data.derive(Serialize, Deserialize)
    pub(all) struct Config {
    #data.rename("max-retries")
    max_retries : Int
    #data.skip
    cache_generation : Int
    }

    Then run the generator, which writes a *_derive.mbt companion next to each source file:

    moonx Yu-zh/data/derive [path ...]

    AttributeOnEffect
    #data.derive(Serialize, Deserialize)struct or enumgenerates the named implementations
    #data.rename("name")field or variantchanges the name on the wire, not in MoonBit
    #data.skipfieldnever written; filled from Default when read

    Generic types get one trait bound per parameter, so Pair[A, B] derives as impl[A : Serialize, B : Serialize] Serialize for Pair[A, B].

    A struct's declared shape picks its form in the data model, and the two degenerate shapes are not spelled as the general one: struct Rgb(Int, Int,Int) is a tuple struct, but struct Meters(Double) is a newtype struct, written as the value it wraps rather than as a sequence of one, and struct Marker {} is a unit struct, written as unit. Formats that draw no distinction still receive what the general form would have written, since the Serializer defaults widen back to it.

    There is no build-system hook — the current moon.pkg format has no pre-build step — so generated files are committed alongside their sources and the generator is re-run by hand. It is idempotent, and CI can check nothing is stale with moon run derive && git diff --exit-code.

    A trait impl also installs its methods as regular methods on the type, so a derived Serialize would otherwise add value.serialize(s) to your package's public interface — an API you did not ask for, in a file you did not write. Each impl comes with an extend declaration that names the promotion, deprecates it and hides it, so deriving neither widens your interface nor leaves warning 79 (implicit_impl_as_method) behind. Reach these through the Serialize and Deserialize bounds, as generated code itself does.

    See the example package for the whole loop: attributed types, their generated implementations, and round-trip tests.

    #Errors carry a path

    Every failure records where in the document it happened, so a bad field in a large payload does not turn into a shrug:

    ///|
    test "errors say where" {
    let bad = "[{\"name\":\"ada\",\"age\":\"thirty-six\"}]"
    try (@json.from_string(bad) : Array[User]) catch {
    e =>
    inspect(
    e,
    content="$[0].age: invalid type: expected a number, found string",
    )
    } noraise {
    _ => fail("expected a type error")
    }
    }

    DeError distinguishes the cases serde does — InvalidType, InvalidValue, InvalidLength, MissingField, UnknownField, UnknownVariant, Eof and DeCustom — so a format never has to invent message strings.

    #Untyped documents

    Value is the data model made concrete. It is what deserialize_any returns, and it round-trips like any other type:

    ///|
    test "Value carries any document" {
    let text = "{\"tags\":[1,-2.5,true,null],\"name\":\"ada\",\"meta\":{}}"
    let value : @data.Value = @json.from_string(text)
    guard value.get("tags") is Some(tags) else { fail("expected a tags field") }
    inspect(tags, content="[1, -2.5, true, null]")
    inspect(@json.to_string(value), content=text)
    // Strings inside are UTF-8 bytes; `text()` is the explicit step to a String.
    guard value.get("name") is Some(name) else { fail("expected a name field") }
    guard name.text() is Some(who) else { fail("expected valid UTF-8") }
    inspect(who, content="ada")
    }

    #JSON specifics

    The transport is UTF-8 Bytes. to_string / from_string are conveniences over to_bytes / from_bytes, the same split serde_json makes between to_vec / from_slice and to_string / from_str.

    ///|
    test "the transport is bytes" {
    assert_eq(@json.to_bytes("é"), b"\x22\xc3\xa9\x22")
    assert_eq((@json.from_bytes(b"[1,2]") : Array[Int]), [1, 2])
    }

    Where JSON is narrower than the data model it follows serde_json: byte strings become arrays of numbers, Char becomes a one-character string, tuples become arrays, and unit becomes null. It departs on two points, deliberately:

    • NaN and infinity raise UnsupportedType instead of silently becoming null, because losing them quietly is worse than failing.
    • A non-string, non-numeric map key raises instead of being coerced.

    #CBOR specifics

    CBOR (RFC 8949) fits the data model more closely than JSON does, because three things JSON has to fake are native to it:

    ///|
    test "what CBOR carries that JSON cannot" {
    // A byte string stays bytes, rather than becoming an array of numbers.
    assert_eq(@cbor.to_bytes(b"\x01\x02"), b"\x42\x01\x02")
    // A map key need not be a string.
    let keyed : Map[Int, String] = { 1: "a" }
    assert_eq((@cbor.from_bytes(@cbor.to_bytes(keyed)) : Map[Int, String]), keyed)
    // Unsigned and negative integers are distinct major types, so a double is
    // never mistaken for an integer that happened to be whole.
    assert_eq(@cbor.to_bytes(1.5), b"\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00")
    }

    Everything else follows serde_cbor: unit is null, Char is a one-character text string, tuples are arrays, newtype structs are transparent, structs are maps with text keys, and enums are externally tagged.

    Writing uses definite lengths wherever a length is known and the shortest head that fits each integer. That is deterministic for a given value but not canonical CBOR — map keys go out in declaration order rather than sorted.

    Reading deliberately accepts more than writing produces, since a decoder that only understood its own output would be useless for interoperating: indefinite-length containers and strings, half-precision floats, and semantic tags. Tags are read and discarded, so a tagged value decodes as its content; Value has no case for a tag, and refusing them outright would reject ordinary documents from encoders that mark their timestamps.

    #MessagePack specifics

    MessagePack is CBOR's closest sibling — self-describing, binary, big-endian, with byte strings and non-string map keys native — and follows rmp-serde on everything the data model does not pin down. Two differences from CBOR matter:

    Every length is explicit. There is no indefinite-length container, so serialize_seq_begin(None) cannot be honoured and raises UnsupportedType. Nothing in this library passes None, so it is unreachable in practice, but it is the clearest illustration of why the hint is an Int?: JSON never knows a length up front, MessagePack can never do without one, and CBOR can spell either.

    Extension types are refused, not skipped. A CBOR tag wraps a value that decodes on its own, so dropping the tag leaves something meaningful. A MessagePack ext is a type byte and an opaque payload with no inner value to fall back to, and Value has no case for one.

    ///|
    test "every value takes its narrowest form" {
    assert_eq(@msgpack.to_bytes(127), b"\x7f")
    assert_eq(@msgpack.to_bytes(128), b"\xcc\x80")
    assert_eq(@msgpack.to_bytes(-32), b"\xe0")
    assert_eq(@msgpack.to_bytes(b"\x01\x02"), b"\xc4\x02\x01\x02")
    }

    #TOML specifics

    The transport is UTF-8 Bytes, as it is for JSON — that is what a TOML file is on disk — with to_string / from_string as thin conveniences over it.

    TOML is the one format here that cannot stream. Within a table every scalar key must appear before the first sub-table header, so field order is not emission order: a struct { a, sub, b } has to emit a, b, then [sub]. And serialize_field hands over a value whose only capability is Serialize, so the serializer cannot tell a scalar from a table without writing it — classification has to wait until the whole table is in hand.

    That does not put the traits out of reach, because they never required emitting as you go: ValueSerializer accumulates into a tree and is a first-class Serializer for doing so. TomlSerializer and TomlDeserializer are backed by it, matching toml::ser::Serializer and toml::de::Deserializer in Rust. Reading is the easier direction — a pull protocol lets a format parse before the first question, which it must, since a TOML sub-table may be defined before its parent.

    ///|
    test "scalars come out before sub-tables" {
    let doc : Map[String, @data.Value] = {
    "name": Str(b"prod"),
    "server": Map([(Str(b"port"), Int(8080L))]),
    }
    inspect(
    @toml.to_string(doc),
    content=(
    #|name = "prod"
    #|
    #|[server]
    #|port = 8080
    #|
    ),
    )
    }

    What TOML cannot represent, and what happens: a None field is omitted (TOML has no null); byte strings, non-string keys and a non-table root all raise UnsupportedType. Reading back, datetimes are refused rather than coerced into strings, since a string that used to be a datetime no longer is one.

    Infinity and NaN survive, unusually — TOML spells them inf, -inf and nan, so this is the one place a format here is wider than JSON.

    #Choosing a format

    self-describingstreamingbytes as bytesnon-string keysnull
    JSONyesyesno, an array of numbersnoyes
    CBORyesyesyesyesyes
    MessagePackyesyesyesyesyes
    TOMLyesno, buffersnonono

    JSON when a human has to read it. CBOR when a standard matters, since it is an IETF RFC. MessagePack when talking to something that already speaks it. TOML for configuration, where the shape is a table and a person edits the file.

    #How this differs from Rust's serde

    Two deviations, both forced by MoonBit having no associated types.

    Compound protocols are flattened. Serde returns a distinct builder type from serialize_seq and friends, which makes it a type error to mix up two open compounds. Without associated types the sub-serializer cannot be named, so sequences, maps, structs and tuple variants are written as _begin / element / _end triples on the serializer itself. Correct nesting becomes a contract rather than a type guarantee — every misuse raises a named error rather than silently producing wrong output.

    Error is concrete. Serde's Error associated type becomes the concrete SerError and DeError, each carrying a Path.

    There is no general value Visitor. MoonBit trait methods may carry their own type parameters, so deserialize_seq_next is generic in the element type and calls T::deserialize directly — which is the job Serde's Visitor and DeserializeSeed do in Rust. Struct fields are the narrow special case: deserialize_field is generic in FieldIdentifier, and derive generates a private semantic enum for that identifier, analogous to Serde's private field enum. A format may feed it an index, raw UTF-8, or an existing String.

    #License

    Apache-2.0

    Deserialize

    pub(open) trait Deserialize {
    fn[D : Deserializer] deserialize(D) -> Self raise DeError
    }

    A data type that can be reconstructed from any Deserializer.

    The mirror of Serialize. Note there is no self parameter: the type is produced, not consumed, so this is invoked as T::deserialize(d) or through a [T : Deserialize] bound.

    Serde routes this through a Visitor because DeserializeSeed has to thread state through a generic callback. Here the recursion lives in the type parameter of deserialize_seq_next and friends, so the deserializer calls T::deserialize directly and no visitor is needed.
    impl Deserialize for Unit
    impl Deserialize for Bool
    impl Deserialize for Byte
    impl Deserialize for Char
    impl Deserialize for Int
    impl Deserialize for UInt
    impl Deserialize for Option[T]
    impl Deserialize for Array[T]
    impl Deserialize for Map[K, V]
    impl Deserialize for Tuple2[A, B]
    impl Deserialize for Tuple3[A, B, C]

    Deserializer

    pub(open) trait Deserializer {
    fn path(Self) -> Path
    fn deserialize_unit(Self) -> Unit raise DeError
    fn deserialize_bool(Self) -> Bool raise DeError
    fn deserialize_byte(Self) -> Byte raise DeError
    fn deserialize_int16(Self) -> Int16 raise DeError
    fn deserialize_uint16(Self) -> UInt16 raise DeError
    fn deserialize_int(Self) -> Int raise DeError
    fn deserialize_uint(Self) -> UInt raise DeError
    fn deserialize_int64(Self) -> Int64 raise DeError
    fn deserialize_uint64(Self) -> UInt64 raise DeError
    fn deserialize_float(Self) -> Float raise DeError
    fn deserialize_double(Self) -> Double raise DeError
    fn deserialize_char(Self) -> Char raise DeError
    fn deserialize_string(Self) -> String raise DeError
    fn deserialize_str_bytes(Self) -> BytesView raise DeError = _
    fn deserialize_bytes(Self) -> Bytes raise DeError
    fn[T : Deserialize] deserialize_option(Self) -> T? raise DeError
    fn deserialize_unit_struct(Self, String) -> Unit raise DeError = _
    fn[T : Deserialize] deserialize_newtype_struct(Self, String) -> T raise DeError = _
    fn deserialize_seq_begin(Self) -> Int? raise DeError
    fn[T : Deserialize] deserialize_seq_next(Self) -> T? raise DeError
    fn deserialize_tuple_begin(Self, Int) -> Unit raise DeError
    fn[T : Deserialize] deserialize_tuple_next(Self) -> T raise DeError
    fn deserialize_tuple_end(Self) -> Unit raise DeError
    fn deserialize_tuple_struct_begin(Self, String, Int) -> Unit raise DeError = _
    fn deserialize_map_begin(Self) -> Int? raise DeError
    fn[K : Deserialize] deserialize_map_next_key(Self) -> K? raise DeError
    fn[V : Deserialize] deserialize_map_value(Self) -> V raise DeError
    fn deserialize_struct_begin(Self, String, ArrayView[String]) -> Unit raise DeError
    fn[F : FieldIdentifier] deserialize_field(Self) -> F? raise DeError = _
    fn deserialize_field_name(Self) -> String? raise DeError
    fn[T : Deserialize] deserialize_field_value(Self) -> T raise DeError
    fn skip_value(Self) -> Unit raise DeError
    fn deserialize_enum_begin(Self, String, ArrayView[String]) -> Int raise DeError
    fn deserialize_unit_variant(Self) -> Unit raise DeError
    fn[T : Deserialize] deserialize_newtype_variant(Self) -> T raise DeError
    fn deserialize_enum_end(Self) -> Unit raise DeError = _
    fn deserialize_any(Self) -> Value raise DeError
    fn is_human_readable(Self) -> Bool = _
    }

    A format that can produce the data model.

    The methods are hints, not commands: the type says what shape it expects and a self-describing format is free to check that against what it actually found. A non-self-describing format has no choice but to trust the hint, which is exactly why the hints exist.

    Compound protocols are flattened onto Self for the same reason as in Serializer. Iteration terminates on None rather than an explicit _end, so a well-formed read of a sequence, map or struct drains it fully.

    FieldIdentifier

    pub(open) trait FieldIdentifier {
    fn from_index(Int) -> Self
    fn from_bytes(BytesView, index_hint : Int) -> Self
    fn from_string(String) -> Self
    fn name(Self) -> String?
    }

    A struct field identifier interpreted by the type being deserialized.

    Derived implementations use a private enum with one case per known field plus an unknown case. The representation stays entirely with the type: formats supply whichever identifier representation they already have, and receive a semantic value rather than a numeric field position.

    from_bytes receives an index_hint because object keys commonly arrive in declaration order. Implementations may try that field first before falling back to their general lookup; the hint must not change the result. from_index uses the order of the names passed to deserialize_struct_begin, which supports positional binary formats. name returns a canonical known name for error paths; unknown identifiers return None so the format can retain the input name.

    Implementations must keep these representations consistent. If name returns Some(canonical) for a value produced by from_bytes, those bytes must be exactly the UTF-8 encoding of canonical; a value produced by from_string must likewise have matched that string. Byte-oriented formats rely on this invariant to reuse canonical without decoding the input.

    Serialize

    pub(open) trait Serialize {
    fn[S : Serializer] serialize(Self, S) -> Unit raise SerError
    }

    A data type that can describe itself to any Serializer.

    This is half of serde's central idea: a type knows its own shape in terms of the data model, and a format knows how to write the data model. Neither knows about the other, so N types and M formats cost N + M implementations instead of N * M.

    serialize is polymorphic in the serializer, so this trait has no trait object form — exactly as in Rust. Use Value when a heterogeneous collection is needed.
    impl Serialize for Unit
    impl Serialize for Bool
    impl Serialize for Byte
    impl Serialize for Char
    impl Serialize for Int
    impl Serialize for Int16
    impl Serialize for Int64
    impl Serialize for UInt
    impl Serialize for UInt16
    impl Serialize for UInt64
    impl Serialize for Float
    impl Serialize for Double
    impl Serialize for String
    impl Serialize for Option[T]
    impl Serialize for FixedArray[T]
    impl Serialize for Bytes
    impl Serialize for Array[T]
    impl Serialize for Map[K, V]
    impl Serialize for Tuple2[A, B]
    impl Serialize for Tuple3[A, B, C]

    Serializer

    pub(open) trait Serializer {
    fn serialize_unit(Self) -> Unit raise SerError
    fn serialize_bool(Self, Bool) -> Unit raise SerError
    fn serialize_byte(Self, Byte) -> Unit raise SerError
    fn serialize_int16(Self, Int16) -> Unit raise SerError
    fn serialize_uint16(Self, UInt16) -> Unit raise SerError
    fn serialize_int(Self, Int) -> Unit raise SerError
    fn serialize_uint(Self, UInt) -> Unit raise SerError
    fn serialize_int64(Self, Int64) -> Unit raise SerError
    fn serialize_uint64(Self, UInt64) -> Unit raise SerError
    fn serialize_float(Self, Float) -> Unit raise SerError
    fn serialize_double(Self, Double) -> Unit raise SerError
    fn serialize_char(Self, Char) -> Unit raise SerError
    fn serialize_string(Self, String) -> Unit raise SerError
    fn serialize_str_bytes(Self, BytesView) -> Unit raise SerError = _
    fn serialize_bytes(Self, Bytes) -> Unit raise SerError
    fn serialize_none(Self) -> Unit raise SerError
    fn[T : Serialize] serialize_some(Self, T) -> Unit raise SerError
    fn serialize_unit_struct(Self, String) -> Unit raise SerError = _
    fn[T : Serialize] serialize_newtype_struct(Self, String, T) -> Unit raise SerError = _
    fn serialize_unit_variant(Self, VariantInfo) -> Unit raise SerError
    fn[T : Serialize] serialize_newtype_variant(Self, VariantInfo, T) -> Unit raise SerError
    fn serialize_seq_begin(Self, Int?) -> Unit raise SerError
    fn[T : Serialize] serialize_seq_element(Self, T) -> Unit raise SerError
    fn serialize_seq_end(Self) -> Unit raise SerError
    fn serialize_tuple_begin(Self, Int) -> Unit raise SerError = _
    fn[T : Serialize] serialize_tuple_element(Self, T) -> Unit raise SerError = _
    fn serialize_tuple_end(Self) -> Unit raise SerError = _
    fn serialize_tuple_struct_begin(Self, String, Int) -> Unit raise SerError = _
    fn[T : Serialize] serialize_tuple_struct_field(Self, T) -> Unit raise SerError = _
    fn serialize_tuple_struct_end(Self) -> Unit raise SerError = _
    fn serialize_map_begin(Self, Int?) -> Unit raise SerError
    fn[K : Serialize] serialize_map_key(Self, K) -> Unit raise SerError
    fn[V : Serialize] serialize_map_value(Self, V) -> Unit raise SerError
    fn serialize_map_end(Self) -> Unit raise SerError
    fn serialize_struct_begin(Self, String, Int) -> Unit raise SerError
    fn[T : Serialize] serialize_field(Self, String, T) -> Unit raise SerError
    fn serialize_struct_end(Self) -> Unit raise SerError
    fn serialize_tuple_variant_begin(Self, VariantInfo, Int) -> Unit raise SerError
    fn[T : Serialize] serialize_tuple_variant_element(Self, T) -> Unit raise SerError = _
    fn serialize_tuple_variant_end(Self) -> Unit raise SerError
    fn serialize_struct_variant_begin(Self, VariantInfo, Int) -> Unit raise SerError
    fn[T : Serialize] serialize_struct_variant_field(Self, String, T) -> Unit raise SerError = _
    fn serialize_struct_variant_end(Self) -> Unit raise SerError
    fn is_human_readable(Self) -> Bool = _
    }

    A format that can receive the data model.

    Serde returns a distinct builder type from serialize_seq, serialize_map and friends, which statically prevents mixing up two open compounds. MoonBit has no associated types, so the compound protocols are flattened onto Self as _begin / element / _end triples. That makes correct nesting a contract rather than a type guarantee: every _begin must be matched by exactly one _end, and implementations that nest must track their own depth.

    Many methods have defaults that widen to a more general part of the data model — tuples fall back to sequences, tuple structs to tuples — so a format only overrides the ones it represents distinctly.

    DeError

    pub(all) suberror DeError {
    InvalidType(path~ : Path, expected~ : String, found~ : String)
    InvalidValue(path~ : Path, expected~ : String, found~ : String)
    InvalidLength(path~ : Path, expected~ : Int, found~ : Int)
    MissingField(path~ : Path, field~ : String)
    UnknownField(path~ : Path, field~ : String, expected~ : ArrayView[String])
    UnknownVariant(path~ : Path, variant~ : String, expected~ : ArrayView[String])
    Eof(path~ : Path)
    DeCustom(path~ : Path, message~ : String)
    }

    Failure raised by a Deserializer.

    Every variant carries the Path at which the failure occurred. The variants mirror serde's de::Error constructors so that formats report errors uniformly rather than each inventing its own message strings.
    impl Show for DeError

    DeError::path

    fn DeError::path(self : DeError) -> Path

    The path at which this error occurred.

    SerError

    pub(all) suberror SerError {
    UnsupportedType(format~ : String, kind~ : String)
    SerCustom(message~ : String)
    }

    Failure raised by a Serializer.

    Serialization fails far less often than deserialization: the value is already well-typed, so the only real failures are a format that cannot represent part of the data model, or an I/O-style failure in the sink.
    impl Show for SerError

    Path

    pub(all) enum Path {
    Root
    Key(Path, String)
    Index(Path, Int)
    } derive(Eq,
    Debug
    )

    A location within the data being serialized or deserialized.

    Deserializers thread a Path through nested values so that a failure deep inside a document reports where it happened, not just what happened. Rendered as a JSONPath-like string: $.users[0].name.
    impl Show for Path

    Path::index

    fn Path::index(self : Path, i : Int) -> Path

    Extends the path with a sequence index.

    Path::key

    fn Path::key(self : Path, name : String) -> Path

    Extends the path with a struct field or map key.

    Value

    pub(all) enum Value {
    Null
    Boolean(Bool)
    Int(Int64)
    UInt(UInt64)
    Double(Double)
    Str(BytesView)
    Bytes(Bytes)
    Seq(ReadOnlyArray[Value])
    Map(ReadOnlyArray[(Value, Value)])
    } derive(Eq,
    Debug
    )

    A self-describing value: the data model made concrete.

    Serialize is polymorphic in its serializer and therefore has no trait object form, so a heterogeneous collection cannot be built out of Serialize values directly. Value fills that gap, and doubles as the result type of Deserializer::deserialize_any.

    Strings are held as UTF-8 BytesViews, not as MoonBit Strings. The data model describes what a document contains, and a document contains text in some encoding — turning that into UTF-16 is a service to one particular consumer, so the consumer that wants it pays for it. It also means a document can be read and written again without ever being transcoded.

    Structs and enum variants collapse into Map here, using serde's default externally-tagged representation: a unit variant becomes Str(name), and any variant with a payload becomes a single-entry Map from the variant name to its payload.

    Seq and Map hold ReadOnlyArray, which makes a value a tree by construction: a cycle needs an already-built node to be pointed back at one of its ancestors, and nothing reachable from a Value can be written to after it is built. That is not only about traversals terminating. The native and LLVM backends are reference-counted, so a cycle would be a leak that no collector ever comes to clear. It also means a value never aliases the array it was built from.
    impl Serialize for Value
    impl Show for Value

    Value::deserialize

    fn[D : Deserializer] Value::deserialize(d : D) -> Value raise DeError

    Value::detach

    fn Value::detach(self : Value) -> Value

    Copies every borrowed string, so the value no longer keeps the document it was parsed from alive.

    Parsing borrows by default, which is free and right when a whole document is being processed. Keeping a handful of fields out of a large one is the case that wants this: without it, a five-byte key can hold ten megabytes reachable.

    Value::get

    fn Value::get(self : Value, key : String) -> Value?

    Looks up a key in a Map value. Returns None for any other shape.

    Takes a String because that is how a key is written in source; it is encoded once, not once per entry.

    Value::serialize

    fn[S : Serializer] Value::serialize(self : Value, s : S) -> Unit raise SerError

    Value::text

    fn Value::text(self : Value) -> String?

    The text of a Str, decoded. None for any other shape.

    The explicit step from the data model's bytes to a MoonBit String.

    Value::type_name

    fn Value::type_name(self : Value) -> String

    A short name for this value's shape, used in InvalidType messages.

    ValueDeserializer

    type ValueDeserializer

    Reads from an in-memory Value.

    Each nested value gets a child deserializer carrying its own Path, so failures report where in the document they happened.

    ValueDeserializer::deserialize_any

    fn ValueDeserializer::deserialize_any(self : ValueDeserializer) -> Value raise DeError

    ValueDeserializer::deserialize_bool

    fn ValueDeserializer::deserialize_bool(self : ValueDeserializer) -> Bool raise DeError

    ValueDeserializer::deserialize_byte

    fn ValueDeserializer::deserialize_byte(self : ValueDeserializer) -> Byte raise DeError

    ValueDeserializer::deserialize_bytes

    fn ValueDeserializer::deserialize_bytes(self : ValueDeserializer) -> Bytes raise DeError

    ValueDeserializer::deserialize_char

    fn ValueDeserializer::deserialize_char(self : ValueDeserializer) -> Char raise DeError

    ValueDeserializer::deserialize_double

    fn ValueDeserializer::deserialize_double(self : ValueDeserializer) -> Double raise DeError

    ValueDeserializer::deserialize_enum_begin

    fn ValueDeserializer::deserialize_enum_begin(self : ValueDeserializer, _name : String, variants : ArrayView[String]) -> Int raise DeError

    ValueDeserializer::deserialize_enum_end

    fn ValueDeserializer::deserialize_enum_end(_self : ValueDeserializer) -> Unit raise DeError

    ValueDeserializer::deserialize_field

    fn[F : FieldIdentifier] ValueDeserializer::deserialize_field(self : ValueDeserializer) -> F? raise DeError

    ValueDeserializer::deserialize_field_name

    fn ValueDeserializer::deserialize_field_name(self : ValueDeserializer) -> String? raise DeError

    ValueDeserializer::deserialize_field_value

    fn[T : Deserialize] ValueDeserializer::deserialize_field_value(self : ValueDeserializer) -> T raise DeError

    ValueDeserializer::deserialize_float

    fn ValueDeserializer::deserialize_float(self : ValueDeserializer) -> Float raise DeError

    ValueDeserializer::deserialize_int

    fn ValueDeserializer::deserialize_int(self : ValueDeserializer) -> Int raise DeError

    ValueDeserializer::deserialize_int16

    fn ValueDeserializer::deserialize_int16(self : ValueDeserializer) -> Int16 raise DeError

    ValueDeserializer::deserialize_int64

    fn ValueDeserializer::deserialize_int64(self : ValueDeserializer) -> Int64 raise DeError

    ValueDeserializer::deserialize_map_begin

    fn ValueDeserializer::deserialize_map_begin(self : ValueDeserializer) -> Int? raise DeError

    ValueDeserializer::deserialize_map_next_key

    fn[K : Deserialize] ValueDeserializer::deserialize_map_next_key(self : ValueDeserializer) -> K? raise DeError

    ValueDeserializer::deserialize_map_value

    fn[V : Deserialize] ValueDeserializer::deserialize_map_value(self : ValueDeserializer) -> V raise DeError

    ValueDeserializer::deserialize_newtype_struct

    fn[T : Deserialize] ValueDeserializer::deserialize_newtype_struct(self : ValueDeserializer, _name : String) -> T raise DeError

    ValueDeserializer::deserialize_newtype_variant

    fn[T : Deserialize] ValueDeserializer::deserialize_newtype_variant(self : ValueDeserializer) -> T raise DeError

    ValueDeserializer::deserialize_option

    fn[T : Deserialize] ValueDeserializer::deserialize_option(self : ValueDeserializer) -> T? raise DeError

    ValueDeserializer::deserialize_seq_begin

    fn ValueDeserializer::deserialize_seq_begin(self : ValueDeserializer) -> Int? raise DeError

    ValueDeserializer::deserialize_seq_next

    fn[T : Deserialize] ValueDeserializer::deserialize_seq_next(self : ValueDeserializer) -> T? raise DeError

    ValueDeserializer::deserialize_str_bytes

    fn ValueDeserializer::deserialize_str_bytes(self : ValueDeserializer) -> BytesView raise DeError

    ValueDeserializer::deserialize_string

    fn ValueDeserializer::deserialize_string(self : ValueDeserializer) -> String raise DeError

    ValueDeserializer::deserialize_struct_begin

    fn ValueDeserializer::deserialize_struct_begin(self : ValueDeserializer, _name : String, _fields : ArrayView[String]) -> Unit raise DeError

    ValueDeserializer::deserialize_tuple_begin

    fn ValueDeserializer::deserialize_tuple_begin(self : ValueDeserializer, _len : Int) -> Unit raise DeError

    ValueDeserializer::deserialize_tuple_end

    fn ValueDeserializer::deserialize_tuple_end(_self : ValueDeserializer) -> Unit raise DeError

    ValueDeserializer::deserialize_tuple_next

    fn[T : Deserialize] ValueDeserializer::deserialize_tuple_next(self : ValueDeserializer) -> T raise DeError

    ValueDeserializer::deserialize_tuple_struct_begin

    fn ValueDeserializer::deserialize_tuple_struct_begin(self : ValueDeserializer, _name : String, len : Int) -> Unit raise DeError

    ValueDeserializer::deserialize_uint

    fn ValueDeserializer::deserialize_uint(self : ValueDeserializer) -> UInt raise DeError

    ValueDeserializer::deserialize_uint16

    fn ValueDeserializer::deserialize_uint16(self : ValueDeserializer) -> UInt16 raise DeError

    ValueDeserializer::deserialize_uint64

    fn ValueDeserializer::deserialize_uint64(self : ValueDeserializer) -> UInt64 raise DeError

    ValueDeserializer::deserialize_unit

    fn ValueDeserializer::deserialize_unit(self : ValueDeserializer) -> Unit raise DeError

    ValueDeserializer::deserialize_unit_struct

    fn ValueDeserializer::deserialize_unit_struct(self : ValueDeserializer, _name : String) -> Unit raise DeError

    ValueDeserializer::deserialize_unit_variant

    fn ValueDeserializer::deserialize_unit_variant(self : ValueDeserializer) -> Unit raise DeError

    ValueDeserializer::is_human_readable

    fn ValueDeserializer::is_human_readable(_self : ValueDeserializer) -> Bool

    ValueDeserializer::new

    fn ValueDeserializer::new(value : Value, path? : Path) -> ValueDeserializer

    ValueDeserializer::path

    ValueDeserializer::skip_value

    fn ValueDeserializer::skip_value(self : ValueDeserializer) -> Unit raise DeError

    ValueSerializer

    type ValueSerializer

    Builds a Value in memory.

    Nested values are serialized by capture, which hands each one a fresh serializer. That keeps at most one compound open per instance, so no explicit nesting stack is needed and a misplaced _element or _end call raises rather than silently corrupting the result.

    ValueSerializer::capture

    fn[T : Serialize] ValueSerializer::capture(value : T) -> Value raise SerError

    Serializes one value in isolation and returns it.

    ValueSerializer::finish

    fn ValueSerializer::finish(self : ValueSerializer) -> Value raise SerError

    The completed value. Raises if nothing was serialized, or if a compound was opened and never closed.

    ValueSerializer::is_human_readable

    fn ValueSerializer::is_human_readable(_self : ValueSerializer) -> Bool

    ValueSerializer::new

    ValueSerializer::serialize_bool

    fn ValueSerializer::serialize_bool(self : ValueSerializer, v : Bool) -> Unit raise SerError

    ValueSerializer::serialize_byte

    fn ValueSerializer::serialize_byte(self : ValueSerializer, v : Byte) -> Unit raise SerError

    ValueSerializer::serialize_bytes

    fn ValueSerializer::serialize_bytes(self : ValueSerializer, v : Bytes) -> Unit raise SerError

    ValueSerializer::serialize_char

    fn ValueSerializer::serialize_char(self : ValueSerializer, v : Char) -> Unit raise SerError

    ValueSerializer::serialize_double

    fn ValueSerializer::serialize_double(self : ValueSerializer, v : Double) -> Unit raise SerError

    ValueSerializer::serialize_field

    fn[T : Serialize] ValueSerializer::serialize_field(self : ValueSerializer, name : String, v : T) -> Unit raise SerError

    ValueSerializer::serialize_float

    fn ValueSerializer::serialize_float(self : ValueSerializer, v : Float) -> Unit raise SerError

    ValueSerializer::serialize_int

    fn ValueSerializer::serialize_int(self : ValueSerializer, v : Int) -> Unit raise SerError

    ValueSerializer::serialize_int16

    fn ValueSerializer::serialize_int16(self : ValueSerializer, v : Int16) -> Unit raise SerError

    ValueSerializer::serialize_int64

    fn ValueSerializer::serialize_int64(self : ValueSerializer, v : Int64) -> Unit raise SerError

    ValueSerializer::serialize_map_begin

    fn ValueSerializer::serialize_map_begin(self : ValueSerializer, _hint : Int?) -> Unit raise SerError

    ValueSerializer::serialize_map_end

    fn ValueSerializer::serialize_map_end(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_map_key

    fn[K : Serialize] ValueSerializer::serialize_map_key(self : ValueSerializer, k : K) -> Unit raise SerError

    ValueSerializer::serialize_map_value

    fn[V : Serialize] ValueSerializer::serialize_map_value(self : ValueSerializer, v : V) -> Unit raise SerError

    ValueSerializer::serialize_newtype_struct

    fn[T : Serialize] ValueSerializer::serialize_newtype_struct(self : ValueSerializer, _name : String, value : T) -> Unit raise SerError

    ValueSerializer::serialize_newtype_variant

    fn[T : Serialize] ValueSerializer::serialize_newtype_variant(self : ValueSerializer, info : VariantInfo, v : T) -> Unit raise SerError

    ValueSerializer::serialize_none

    fn ValueSerializer::serialize_none(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_seq_begin

    fn ValueSerializer::serialize_seq_begin(self : ValueSerializer, hint : Int?) -> Unit raise SerError

    ValueSerializer::serialize_seq_element

    fn[T : Serialize] ValueSerializer::serialize_seq_element(self : ValueSerializer, v : T) -> Unit raise SerError

    ValueSerializer::serialize_seq_end

    fn ValueSerializer::serialize_seq_end(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_some

    fn[T : Serialize] ValueSerializer::serialize_some(self : ValueSerializer, v : T) -> Unit raise SerError

    ValueSerializer::serialize_str_bytes

    fn ValueSerializer::serialize_str_bytes(self : ValueSerializer, v : BytesView) -> Unit raise SerError

    ValueSerializer::serialize_string

    fn ValueSerializer::serialize_string(self : ValueSerializer, v : String) -> Unit raise SerError

    ValueSerializer::serialize_struct_begin

    fn ValueSerializer::serialize_struct_begin(self : ValueSerializer, _name : String, _len : Int) -> Unit raise SerError

    ValueSerializer::serialize_struct_end

    fn ValueSerializer::serialize_struct_end(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_struct_variant_begin

    fn ValueSerializer::serialize_struct_variant_begin(self : ValueSerializer, info : VariantInfo, _len : Int) -> Unit raise SerError

    ValueSerializer::serialize_struct_variant_end

    fn ValueSerializer::serialize_struct_variant_end(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_struct_variant_field

    fn[T : Serialize] ValueSerializer::serialize_struct_variant_field(self : ValueSerializer, name : String, value : T) -> Unit raise SerError

    ValueSerializer::serialize_tuple_begin

    fn ValueSerializer::serialize_tuple_begin(self : ValueSerializer, len : Int) -> Unit raise SerError

    ValueSerializer::serialize_tuple_element

    fn[T : Serialize] ValueSerializer::serialize_tuple_element(self : ValueSerializer, value : T) -> Unit raise SerError

    ValueSerializer::serialize_tuple_end

    fn ValueSerializer::serialize_tuple_end(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_tuple_struct_begin

    fn ValueSerializer::serialize_tuple_struct_begin(self : ValueSerializer, _name : String, len : Int) -> Unit raise SerError

    ValueSerializer::serialize_tuple_struct_end

    fn ValueSerializer::serialize_tuple_struct_end(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_tuple_struct_field

    fn[T : Serialize] ValueSerializer::serialize_tuple_struct_field(self : ValueSerializer, value : T) -> Unit raise SerError

    ValueSerializer::serialize_tuple_variant_begin

    fn ValueSerializer::serialize_tuple_variant_begin(self : ValueSerializer, info : VariantInfo, len : Int) -> Unit raise SerError

    ValueSerializer::serialize_tuple_variant_element

    fn[T : Serialize] ValueSerializer::serialize_tuple_variant_element(self : ValueSerializer, value : T) -> Unit raise SerError

    ValueSerializer::serialize_tuple_variant_end

    fn ValueSerializer::serialize_tuple_variant_end(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_uint

    fn ValueSerializer::serialize_uint(self : ValueSerializer, v : UInt) -> Unit raise SerError

    ValueSerializer::serialize_uint16

    fn ValueSerializer::serialize_uint16(self : ValueSerializer, v : UInt16) -> Unit raise SerError

    ValueSerializer::serialize_uint64

    fn ValueSerializer::serialize_uint64(self : ValueSerializer, v : UInt64) -> Unit raise SerError

    ValueSerializer::serialize_unit

    fn ValueSerializer::serialize_unit(self : ValueSerializer) -> Unit raise SerError

    ValueSerializer::serialize_unit_struct

    fn ValueSerializer::serialize_unit_struct(self : ValueSerializer, _name : String) -> Unit raise SerError

    ValueSerializer::serialize_unit_variant

    fn ValueSerializer::serialize_unit_variant(self : ValueSerializer, info : VariantInfo) -> Unit raise SerError

    VariantInfo

    pub(all) struct VariantInfo {
    enum_name : String
    index : Int
    name : String
    }

    Identifies one variant of an enum within the data model.

    Serde passes (name, variant_index, variant) positionally to four separate methods; bundling them keeps the Serializer signatures readable and makes it impossible to transpose the two names by accident.

    from_value

    fn[T : Deserialize] from_value(value : Value) -> T raise DeError

    Reconstructs a value from a Value.

    required

    fn[T] required(field : T?, name~ : String, path~ : Path) -> T raise DeError

    Raises MissingField when a struct field was never seen.

    Hand-written Deserialize implementations accumulate fields into options and finish by unwrapping them through this.

    to_value

    fn[T : Serialize] to_value(value : T) -> Value raise SerError

    Serializes any value into a Value.