hd-lang

#Serde

Status: standard library specification draft.

This chapter defines the part of std.serde that lib/std writes in ordinary hd over the language tier:

  • the data model: what each Serializer call writes, and what each Deserializer call reads;
  • what a derived Serialize or Deserialize calls, through the traits' templates;
  • the standard implementations of both traits.

The language tier keeps the module's declarations and what opting in means (Serialization), since the host boundary reads the implementation's output, not the field tree (module.boundary.out).

#Data Model

A format sees one value as a tree of calls on its writer:

use std.serde.{Serialize, Serializer}

fn write_pair[W < Serializer](out: &mut W, name: string, count: i64) -> Result[void, W::Error]:
    out.begin_list(2)?
    out.text(name)?
    out.int(count)?
    out.end_list()
Rule Value Serializer calls
std-serde.model.nullNull an absent value null()
std-serde.model.boolBoolean false or true bool(value)
std-serde.model.intSigned an integer that fits i64 int(value)
std-serde.model.uintUnsigned an integer of an unsigned type uint(value)
std-serde.model.floatFloat any f64, NaN and infinities included float(value)
std-serde.model.textText a string text(value)
std-serde.model.listList items in order begin_list(len), one value per item, end_list()
std-serde.model.mapMap entries in order, each with a string key begin_map(len), then per entry key(k) and one value, then end_map()
std-serde.model.variantVariant a struct value, or an enum value begin_variant(facts, v), then per member member(m) and one value, then end_variant()
  1. std-serde.model.one-valueA serialize call writes exactly one value of the table: one scalar call, or one begin call, its contents, and the matching end call.
  2. std-serde.model.lenThe len of begin_list or begin_map is the number of items or entries that follow.
  3. std-serde.model.variant-args.local-refbegin_variant(facts: Facts, v: &VariantInfo) receives the type-level facts and a local reference to the variant's metadata.
  4. std-serde.model.data-variantA struct value has one variant, whose of_data is true.
  5. std-serde.model.member-args.local-refmember(m: &Member) receives a local reference to the metadata of the member whose value follows.
  6. std-serde.model.member-factsThe member's facts are those visible to its derivation, as annot.structure.per-derivation specifies.
  7. std-serde.model.errorsA writer's first .Err ends the write. Every standard implementation returns it unchanged, and calls nothing after it.
  8. std-serde.model.ref-receiversEvery method of Serializer and Deserializer takes &mut self, and serialize and deserialize take the writer or reader as &mut W or &mut R. So a format's writer or reader is a struct that changes in place, and its caller reads the struct's fields after the call.

Why. Fourteen calls cover JSON's kinds, and the variant calls keep names and facts, which a format needs to choose a key or a tag. Fewer calls than serde's 29 keep a format small, and a binary format can still tell int from uint.

#Reading

A Deserializer reads the same tree, one call per value:

Rule Call Returns
std-serde.read.peekpeek peek() the ValueKind of the next value, without reading it; an absent value is Null
std-serde.read.is-nullis_null is_null() true when the next value is null or absent, which reads it; false otherwise, which leaves it
std-serde.read.scalarScalars bool, int, uint, float, text the next value, when it is of that kind and fits the result type
std-serde.read.listList begin_list(), then next_item() before each item next_item is true when an item follows, and false after the last
std-serde.read.mapMap begin_map(), then next_key() before each entry next_key is the entry's key, or .None after the last
std-serde.read.variantVariant begin_variant(facts, choices), then per member member(m) and one value, then end_variant() begin_variant is the index in choices of the variant read
std-serde.read.invalidinvalid invalid(expected) the format's error for the value just read, which does not fit expected
std-serde.read.absentis_absent is_absent() true when the member that member(m) just positioned has no value at all, which reads nothing; the trait's default body gives false
  1. std-serde.read.one-valueA deserialize call reads exactly one value.
  2. std-serde.read.expectedEach scalar call's expected names the type being read, as "u8". A format's error for a value of another kind may report it.
  3. std-serde.read.member.local-refA member(m: &Member) call positions the next value of that member, which the format may report as absent.
  4. std-serde.read.choiceschoices holds the VariantInfo of every variant of the type, in declaration order. A struct offers its one variant.
use std.serde.Deserializer

fn read_text_or_empty[R < Deserializer](input: &mut R) -> Result[string, R::Error]:
    match input.peek()?:
        .Text => input.text("string")
        _ => .Ok("")

Note. peek lets a value that holds any kind, such as a Json, read itself. A format that is not self-describing may return an error from peek.

#Derived Implementations

The templates drive a format's writer or reader with a value's members. For the Session of Serialization:

Value Serializer calls of derived serialize
Session { user: "ada", token: "t", expires_at: 9 } begin_variant, member for user, text("ada"), member for token, text("t"), member for expires_at, int(9), end_variant()
  1. std-serde.derive.serialize.templatestd.serde declares the template of Serialize, which @derive(Serialize) instantiates.
  2. std-serde.derive.serialize.included-orderDerived serialize calls begin_variant, then member(m) and serialize for each included walked member in declaration order, then end_variant().
  3. std-serde.derive.deserialize.templatestd.serde declares the template of Deserialize, which @derive(Deserialize) instantiates.
  4. std-serde.derive.deserialize.unskipped-orderDerived deserialize offers every variant to begin_variant, then calls member(m) and deserialize for each unskipped built member in declaration order, then end_variant().
  5. std-serde.derive.deserialize.unskipped-defaultAfter member(m), derived deserialization asks is_absent() for an unskipped defaulted member, using its declared default instead of deserialize when absent.
  6. std-serde.derive.omittedA member omitted with = pass is not written. Reading fills it with its declared default, as annot.omit.default states.
  7. std-serde.derive.sharedShared constructor data is not a member, so it is neither written nor read.
use std.serde.{Serialize, Deserialize}
use std.structure.Structure

struct Cache:
    hits: i64

struct Session:
    pub user: string
    token: string
    cache: Cache = Cache { hits: 0 }

impl Serialize for Session by Structure:
    cache = pass

impl Deserialize for Session by Structure:
    cache = pass

Note. The two blocks above leave cache out of every format. Their member lines agree, so they get no derivation-line-drift warning.

#Member Names

A member's wire name is its name in a document format. For example, @rename("total") on total_cents: i64 uses the key total.

  1. std-serde.rename.factstd.serde exports the field fact Rename with the public field name: string.

  2. std-serde.rename.constructorrename(name: string) -> Rename constructs a fact with that name.

  3. std-serde.member-name.precedenceA member's wire name uses its Rename.name first, its container's RenameAll style second, and its source name otherwise.

  4. std-serde.rename.metadataMember.name retains the source member name, and document formats read Rename from Member.facts.

  5. std-serde.member-name.helper.local-refmember_name(member: &Member, container: Facts) -> string returns that member's wire name.

  6. std-serde.member-name.formatsStandard serializers and deserializers use member_name for document or capture-group names.

  7. std-serde.rename-all.stylesRenameCase is an Eq and Debug enum with the eight style variants in the table.

  8. std-serde.rename-all.factRenameAll is a data-type and enum fact with the public field style: RenameCase.

  9. std-serde.rename-all.constructorrename_all(style: RenameCase) -> RenameAll constructs the fact.

  10. std-serde.rename-all.conversionStyle conversion follows the table, applying ASCII case changes to a member's written name.

  11. std-serde.rename-all.containerA standard format's container facts are the current record's or enum's per-derivation facts supplied to begin_variant.

  12. std-serde.rename-all.map-keysA RenameAll style does not change string map keys.

RenameCase variant Style Conversion order_id becomes
Lowercase lowercase Keep the name order_id
Uppercase UPPERCASE Uppercase ASCII letters ORDER_ID
PascalCase PascalCase Remove underscores and uppercase each word's first character in ASCII OrderId
CamelCase camelCase PascalCase, then lowercase its first character in ASCII orderId
SnakeCase snake_case Keep the name order_id
ScreamingSnakeCase SCREAMING_SNAKE_CASE Uppercase ASCII letters ORDER_ID
KebabCase kebab-case Replace underscores with hyphens order-id
ScreamingKebabCase SCREAMING-KEBAB-CASE Uppercase ASCII letters and replace underscores with hyphens ORDER-ID

For example, @rename_all(.CamelCase) on OrderPaid gives the member order_id the wire name orderId; a field's explicit @rename wins.

Why. Document names belong to the serialization view; preserving the source name lets other adapters, such as environment readers, name fields independently.

#Absent Optional Members

The typed SkipNone fact omits an optional member only when it is absent. For example, @skip_none() on note: string? omits the key for .None.

  1. std-serde.skip-none.factstd.serde exports the member-typed field fact SkipNone[F] for the field type F.
  2. std-serde.skip-none.constructorskip_none[T]() -> SkipNone[T?] constructs the fact for an optional field, checked by annot.typed-fact.check.
  3. std-serde.skip-none.omitDerived serialization omits a member marked by skip_none() when its value is .None, without calling its serializer or the format's member.
  4. std-serde.skip-none.presentA marked .Some(value) is written normally, including .Some(.None) as a present member whose value is null.
  5. std-serde.skip-none.readSkipNone does not change deserialization, so absent keys follow the ordinary optional and declared-default rules.
  6. std-serde.skip-none.ordinaryAn unmarked optional member still writes .None as null.

Why. The fact's optional predicate runs before the format sees the member, so every serializer receives the same view without confusing a present value that serializes as null with an absent optional.

#Skipped Members

The Skip field fact omits a member from its serde view. For example, @skip() on cached: usize = 0 leaves the cache out of documents.

  1. std-serde.skip.factstd.serde exports the field fact Skip with no fields.
  2. std-serde.skip.constructorskip() -> Skip constructs the fact.
  3. std-serde.skip.writeDerived serialization omits a member with Skip without calling its serializer or the format's member method for it.
  4. std-serde.skip.readDerived deserialization fills a skipped member from its declared default without positioning or decoding its input, even when a key is present.
  5. std-serde.skip.impl-factThe walker of derived serialization and the source of derived deserialization carry skips::[Skip](). So annot.impl-fact.skips omits a member whose known facts hold Skip.
  6. std-serde.skip.no-boundA skipped field's type need not implement Serialize or Deserialize.
  7. std-serde.skip.no-defaultUnder a derived Deserialize, a skipped field without a declared default is an error at the opt-in. Error: omitted-member-without-default.
  8. std-serde.skip.unknown-factA Skip that only a named list or a spread attaches is read when the program runs. The field keeps both bounds, and reading it without a declared default returns a deserializer error naming the field.

Why. A skipped member is never written or read, so its type owes serde nothing. Saying so on the walker and source keeps the rule in library code.

#Standard Implementations

std.serde implements both traits for these types:

Rule Type serialize writes deserialize reads, with expected
std-serde.std.boolBoolean bool bool bool, "bool"
std-serde.std.textString string text text, "string"
std-serde.std.charCharacter char text of that one character text of exactly one character, "char"
std-serde.std.signedSigned integers i8, i16, i32, and i64 int int within the type's range, the type's name
std-serde.std.unsignedUnsigned integers u8, u16, u32, u64, and usize uint uint within the type's range, the type's name
std-serde.std.floatFloats f32 and f64 float of the value as an f64 float, the type's name
std-serde.std.optionalOptional T?, where T implements the trait null for .None, and the payload for .Some .None when is_null is true, and .Some of what T reads otherwise
std-serde.std.listList List[T], where T implements the trait a list of the items, in order a list, item by item
std-serde.std.map-entriesMap Map[string, V], where V implements the trait a map of the entries, in the map's iteration order a map, entry by entry, in its order
std-serde.std.setSet Set[T], where T implements the trait a list of the elements, in insertion order a list, element by element; a repeated element keeps one entry
std-serde.std.tupleTuple a tuple type whose members implement the trait a list of the members, in order a list with one item per member, in order
std-serde.std.referenceReference &T and &mut T, where T implements the trait what T writes for the value the reference refers to a new object that holds what T reads
  1. std-serde.std.rangeA value that its scalar call reads but the type cannot hold is the reader's invalid(expected) error. That is an integer outside the type's range, a char text of another length, or an f32 past its finite range. The range of usize is the target's, by types.usize.width, so on the Wasm32 target it is that of u32.
  2. std-serde.std.float-readAn f32 reads the nearest f32 to the f64 it reads.
  3. std-serde.std.tuple-lengthA tuple reads exactly one list item per member. A list with fewer items, or with items left after the last member, is the reader's invalid(expected) error, so JSON reports it at the path of the list or of the first extra item.
  4. std-serde.std.tuple-restA rest element is one member of type List[T], by annot.walk.rest.default. So (i32, List[i32]...) writes [1, [2, 3]], a list whose last item is the rest list.
  5. std-serde.std.map-keysOnly a Map whose key type is string implements the traits. Using a map with any other key type where they are required is an error. Error: unsatisfied-trait-bound.
Error example
use std.serde.Serialize

fn send[T < Serialize](value: T) -> void:
    pass

fn send_counts(counts: Map[i32, i32]) -> void:
    send(counts)  # error: unsatisfied-trait-bound

Note. T?? loses a layer: .Some(.None) writes null, which reads back as .None.

Why. A map key is text in JSON, YAML, and TOML. A map with other keys converts them first, so the program chooses their text.

See also: Serialization, Typed JSON, Time, Boundary Encoding.