Scarlet Industries

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.