JSON
A cursor over a compact tape rather than a tree of values. Nothing is materialised until it is read.
Parsing
json.parse reads bytes with SIMD and returns a
Doc: a cursor over a compact tape, not a tree of values.
Nothing is materialised until it is read, so a program that reads 6
fields out of a 400-field payload pays for 6 fields.
payload = '{"id":"80351110224678912","name":"ada","score":9.5}'
match json.parse(payload) {
Ok(doc) -> println('${option.then(json.field(doc, 'name'), json.string)}')
Err(e) -> println(json.parse_error_message(e))
}
Doc is opaque. Only the parser makes one, so every read
starts from a document that parsed.
Decoders
A Decoder(a) turns a document into your own type.
Combinators compose them; decode.run applies one.
type User {
id Int
name String
score Float
}
fn user() decode.Decoder(User) {
id <- decode.take('id', decode.int_string())
name <- decode.take('name', decode.string_value())
score <- decode.take('score', decode.float())
decode.succeed(User(id, name, score))
}
Failures accumulate and carry the path they happened at. A malformed payload names every field that was wrong, in one pass.
bad = '{"id":7,"name":42,"score":9.5}'
// .id: expected a quoted integer, found an integer
// .name: expected a string, found an integer
Absent, null and present
A member of a JSON object is in one of 3 states, and a partial update means something different for each. An omitted member means unchanged. A null member means cleared. A present member carries the new value.
A decoder that returns Option has already collapsed the
first 2, and the first partial update it reads destroys data. So
decode.optional_field returns Field(a).
Field is opaque: it cannot be matched on, and there is no
function from Field(a) to Option(a). Consume
it with field.fold, which takes one branch per state, or
with field.update, which is the partial-update rule
itself.
// An omitted field means "unchanged". A null field means "cleared". They are
// different, so `optional_field` returns `Field(a)` and not `Option(a)`.
fn patch() decode.Decoder(field.Field(String)) {
decode.optional_field('nickname', decode.string_value())
}
fn apply(src String, current Option(String)) Option(String) {
match json.parse(src) {
Err(_) -> current
Ok(doc) -> match decode.run(patch(), doc) {
Err(_) -> current
Ok(f) -> field.update(f, current)
}
}
}
fn held() Option(String) {
Some('lovelace')
}
// {"name":"ada"} -> Some(lovelace) unchanged
// {"name":"ada","nickname":null} -> None cleared
// {"name":"ada","nickname":"count"} -> Some(count) set
field.update takes the value it must preserve, so the
unchanged branch has something to leave. That is what makes it
incapable of losing data.
Encoding
json.Json is the constructible tree, for documents you
send. json.encode serializes it. The constructors are
Boolean, Integer and Real:
Bool, Int and Float are prelude
type names and cannot be redefined.
document = json.Object(
[
('name', json.Str('ada')),
('id', json.Integer(80351110224678912)),
('score', json.Real(9.5)),
('active', json.Boolean(True)),
('nickname', json.Null),
],
)
println(json.encode(document))
The parser reads RFC 8259 with no extensions. Comments, trailing
commas, bare NaN, and trailing bytes after the top-level
value are all errors, as is an unpaired UTF-16 surrogate escape.
Duplicate keys are kept: json.field returns the first
match and json.entries returns every member. A JSON
integer above 9223372036854775807 has kind Int and no
Int value — json.int reports it absent rather
than returning a truncated number.