data

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

serde
serialization
json
derive
moon add Yu-zh/data@0.1.0
Download zip
Author
Version
0.1.0
License
Apache-2.0
Last updated
2 days ago
Downloads
4

Dependencies

README

#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.

///|
#dataderive(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

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()
}

///|
pub impl @data.Deserialize for User with fn deserialize(d) {
d.deserialize_struct_begin("User", ["name", "age", "nickname"])
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", d.path()),
age: @data.required(age, "age", 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.

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:

///|
#dataderive(Serialize, Deserialize)
pub(all) struct Config {
#datarename("max-retries")
max_retries : Int
#dataskip
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].

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.

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],\"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)
}

#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.

#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.

And one thing serde needs that this does not: there is no 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 Visitor and DeserializeSeed exist to do in Rust.

#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_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, Array[String]) -> Unit 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, Array[String]) -> String 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.

#
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_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~ : Array[String])
UnknownVariant(path~ : Path, variant~ : String, expected~ : Array[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(String)
Bytes(Bytes)
Seq(Array[Value])
Map(Array[(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.

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.
impl Serialize for Value
impl Show for Value

#
Value::get

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

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

#
Value::type_name

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

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

#
ValueDeserializer

pub struct ValueDeserializer {
value : Value
path : Path
cursor : Int
}

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::new

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

#
ValueSerializer

pub struct ValueSerializer {
out : Value?
items : Array[Value]?
entries : Array[(Value, Value)]?
pending_key : Value?
variant : String?
}

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::new

#
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.