Encode and Decode
A value is its constructor call, offered in three forms - Encode writes it, Decode reads it back, Describe describes it without a value - so a type works with every format without hand-written serialization code.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
What reflection is normally needed for - serialization, config mapping, database rows, schemas, debug output - is
covered by the type's constructor, which the compiler offers in three generated forms, the same way Equals, Hash
and Show are generated for a type without being written.
Example#
type User {
name: String
tags: List<String> = []
}
const json = Json()
const text = json.encode User(name: "Ada", tags: ["math"])
print text
print json.decode<User>(text)Syntax#
trait Encode { fn encode<Target: Encoder>(var target: Target) }
trait Decode { static fn decode<Source: Decoder>(var source: Source): Result<Self, DecodeError> }
trait Describe { static fn describe<Target: Describer>(var target: Target) }Rules#
All three are generated when the constructor is usable from outside, over exactly its parameters. Every parameter's type has to have the trait itself. So what is written can always be read back: both forms are the one constructor.
A
privatefield with a default is not a parameter from outside, so it is in none of the three forms. A cache or a memo stays out without an annotation; on the way in it takes its default. Aprotected varfield is publicly constructible and therefore part of all three.A capsule - a
privatefield without a default - is written, read and described as the source of its one conversion pair.SelfhasFrom<Source>orTryFrom<Source, Failure>andSourcehasFrom<Self>; a failingtryFrombecomes theDecodeError; with several pairs, the one withStringis taken. Without a pair there is noDecodeand noDescribe, andEncodestays field by field (see Data or capsule).type ParseError { message: String } type Email with TryFrom<String, ParseError> { private value: String static fn tryFrom(text: String): Result<Email, ParseError> { if !text.contains("@") { return Fail ParseError("'{text}' is not an email address") } Ok Self(text) } } extend Email with Encode, Decode { fn encode<Target: Encoder>(var target: Target) { target.string value } static fn decode<Source: Decoder>(var source: Source): Result<Email, DecodeError> { const text = source.string()? Email.tryFrom(text).mapError { DecodeError _.message } } } const json = Json() const encoded = json.encode(Email.tryFrom("ada@example.test")?) print json.decode<Email>(encoded)A field with a default may be missing on the way in. The derived
decodeevaluates the default only then, so a value that is present never pays for one.A format implements
Encoder,DecoderorDescriberand never sees a type.Json, a TOML reader or a database driver is one implementation each; a type that describes itself once works with every format that exists, and a new format works with every type that exists, which is N + M implementations instead of N × M.What the generated code does is exactly what could be written by hand. A record announces itself with
record(typeName), then every parameter asfield(name)followed by its value's ownencode, thenfinish(), in declaration order; a type with cases writesvariant(typeName, name)for the case it is.typeNameis the declaration's qualified name, such as"app/orders/Price", which is what a format finds a mapping by.What is special about one type in one format belongs to the format. A different spelling of every field name is an option of the format (
Json(naming: .SnakeCase)); a different representation in every format isencodeanddecodewritten by hand. There are no annotations.There is no tree in between. Values are written while the type describes itself: nothing is lost on the way (a narrow number keeps its range, a
Setcomes back as aSetbecause the target type drives the decoding), and a sequence can be streamed one element at a time instead of being held in memory whole.A literal type is read as its base and then checked against its members.
"draft" | "sent"is aStringat run time, but itsdecodeis its own: a text that is none of the members fails with the type'sLiteralParseError, so a document cannot put a value into a literal type thattryFromwould refuse.A
DecodeErrornames the whole chain it went wrong in. A field follows a., a position of a sequence or a tuple stands in brackets, an entry of a map follows a.with its key:items[2].price: a value is needed.The derived
decodereads past a field that failed and fails once at the end, so a document with three mistakes says three: the error'sproblemsholds oneDecodeErrorper failed field, nested records and list items included, each with its whole path. A single problem shows as before, several as one line each. A hand-writtendecodeand a failingtryFromstay one problem.type Price { amount: Int currency: String } const json = Json() print json.decode<Price>(raw"""{"amount": "ten"}""") // prints Fail(amount: a whole number is needed, not "ten" // prints currency: a value is needed)A hand-written
encode,decodeordescribetakes the format as a type parameter, exactly as the trait declares it.fn encode(var target: Encoder)takes a trait-typed value instead and is another member: the checker says it does not matchEncode.
What this is not#
Encode/Decode are not a second, dynamically typed representation of a value. Looking at a value without its
type uses EncodedValue, which is one ordinary format among many, and a document without a known shape uses a library
type such as JsonValue. Neither is something the language treats specially.
use JsonValue from "std/json"
type User {
name: String
}
const user = User "Ada"
const value: JsonValue = Json().value(user)
print value
print EncodedValue.of(user)type Handler {
onClick: () => Void
}
const handler = Handler(onClick: {})
print Json().encode(handler)
// error: `Handler` does not implement `Encode`A field that is a function breaks Encode the same way it breaks Equals, Hash and Show: a function value has
none of them, so the type gets none either.
Related#
- There is no reflection - why generated forms replace a type-level lookup.
- Encoder and Decoder - the scalars and the four shapes every format implements.
- Declaring a type - the other members generated the same way.
- Construction - why the three forms need a usable constructor.