Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Json

JSON: a dynamic value tree, a total parser, a canonical encoder, and a typed layer.

Two layers, following the wire discipline of lib/std/Wire.pr:

  • The dynamic layer is the Json tree and a total decode/encode pair for payloads whose shape you do not control. decode never partially succeeds: a malformed input is one JsonError carrying a message and a line/column, never a panic or a truncated value. Nesting is depth-limited, and any input left over after the top-level value is rejected. - The typed layer is the ToJson/FromJson classes with instances for the base types and the containers, so a declared type converts to and from a Json tree structurally. (Derivation is a manual typeclass rather than deriving (Json): the derivable set is fixed, so a new derivable class is a compiler change; the class-and-instances form is pure library code.)

The JSON contract is deliberately narrow:

  • Number semantics are exact-or-error by default. An integer literal decodes to JInt (Int is arbitrary precision, so no integer overflows); a number decodes to JFloat only when its lexeme is already the canonical form the owned formatter would print (show_int / show_float). A non-canonical spelling (1.0, 1e3, 0.10) is a decode error under decode, and accepted, normalized, under decode_lossy. No silent rounding, no silent 1e999-to-infinity. - Encoding is canonical and byte-deterministic: object keys are sorted, every number is printed by the owned formatter, strings use one fixed escaping, and there are no whitespace options. Two machines encoding equal values produce equal bytes, so a Json value has a well-defined hash and diff, and decode(encode(v)) returns v (with the one deliberate normalization that an integer-valued JFloat prints as an integer and decodes back to JInt, since JSON does not distinguish the two).

Strings are validated UTF-8 by construction (the String type), so invalid UTF-8 is rejected at the byte boundary that builds the input, before the parser ever runs. Streaming (gigabyte, SAX-style) JSON is a package; this codec is value-oriented and total.

Types

Json

type Json
  = JNull
  | JBool(Bool)
  | JInt(Int)
  | JFloat(Float)
  | JStr(String)
  | JArr(List(Json))
  | JObj(List((String, Json)))

A dynamic JSON value. Numbers split into JInt and JFloat so the exact-or-error decode is a structural distinction rather than a hidden flag: an object is an association list in parse order (duplicate keys are preserved, not merged).

encode(JObj([("ok", JBool(true)), ("n", JInt(3))]))
{"n":3,"ok":true}

JsonError

type JsonError = JsonError(String, Int, Int)

A decode failure: a human-readable message and the 1-based line and column of the offending byte.

Type Classes

ToJson

class ToJson(a)
  to_json : (a) -> Json

Convert a value to a Json tree.

deriving (ToJson) writes the instance structurally, for a type whose schema is its own declaration. One constructor becomes one object: a record constructor’s keys are its declared field names, a positional one’s are its argument positions (_0, _1), and a sum additionally names the variant it holds under the key $, which no field name can spell. A document therefore names the constructor it holds rather than an index that quietly changes meaning when a constructor is inserted; a single-constructor type has nothing to discriminate and carries no tag. Constructor and field order are the declaration’s, so one value has one tree, and encode sorts keys, so it has one string. Derive it in a pair with FromJson: a type that encodes but cannot decode is a document nobody can read back.

This is not the wire codec. A Wire.Serialize byte format is frozen and versioned; a JSON document is read by something that was not compiled against this program, so the encoding is self-describing rather than compact, and nothing here promises stability across a change to the declaration.

to_json([1, 2, 3])
Json.JArr([Json.JInt(1), Json.JInt(2), Json.JInt(3)])

FromJson

class FromJson(a)
  from_json : (Json) -> a ! {Fail | e}

Recover a value from a Json tree, failing (through Fail) on a structural mismatch. A decode of foreign data is one ordinary failure channel.

deriving (FromJson) reads back exactly what the derived ToJson wrote, by the same keys: from_json(to_json(x)) is x. A tree that is not an object, a sum whose $ names no constructor of the type, a missing key, and a field that will not itself decode all leave through the same Fail. That failure carries no payload, so it reports that the document did not fit and not where: Fail is a nullary operation, and reporting a path would mean a different effect on this class’s signature and so on every hand-written instance too. Catch it with optional, default, or succeeds, as with any other Fail.

from_json(JInt(41)) + 1
42

Instances

toJsonInt

instance toJsonInt : ToJson(Int)

toJsonFloat

instance toJsonFloat : ToJson(Float)

toJsonBool

instance toJsonBool : ToJson(Bool)

toJsonString

instance toJsonString : ToJson(String)

toJsonList

instance toJsonList : ToJson(List(a))

toJsonOption

instance toJsonOption : ToJson(Option(a))

toJsonPair

instance toJsonPair : ToJson((a, b))

fromJsonInt

instance fromJsonInt : FromJson(Int)

fromJsonFloat

instance fromJsonFloat : FromJson(Float)

fromJsonBool

instance fromJsonBool : FromJson(Bool)

fromJsonString

instance fromJsonString : FromJson(String)

fromJsonList

instance fromJsonList : FromJson(List(a))

fromJsonOption

instance fromJsonOption : FromJson(Option(a))

fromJsonPair

instance fromJsonPair : FromJson((a, b))

Functions and Values

json_error_message

json_error_message : (Json.JsonError) -> String

Render a JsonError as line L col C: message.

json_error_message(JsonError("unexpected character", 1, 5))
line 1 col 5: unexpected character

decode

decode : (String) -> Result(Json.Json, Json.JsonError)

Decode a JSON document with exact number semantics: a number decodes only when its lexeme is already canonical, otherwise a decode error. Total: any malformed or lossy input is an Err with a position.

decode("[1, 2, 3]")
Ok(Json.JArr([Json.JInt(1), Json.JInt(2), Json.JInt(3)]))

decode_lossy

decode_lossy : (String) -> Result(Json.Json, Json.JsonError)

Decode a JSON document, accepting any well-formed number and normalizing it to JInt (exact integer in range) or the nearest JFloat. Still total, and still rejects structurally malformed input.

decode_lossy("1e3")
Ok(Json.JFloat(1000))

encode

encode : (Json.Json) -> String

Encode a Json value to its canonical byte-deterministic string: object keys sorted, numbers by the owned formatter, one fixed string escaping, no optional whitespace. Equal values encode to equal bytes.

encode(JObj([("b", JInt(2)), ("a", JInt(1))]))
{"a":1,"b":2}

json_field

json_field : (List((String, Json.Json)), String) -> Json.Json ! {Fail}

The member named key of an object’s member list, or fail() when there is none. Members are kept in parse order and duplicates are preserved, so the first occurrence wins, which is what makes a decode a function of the document rather than of a hash order. deriving (ToJson, FromJson) reads every field through this, so a missing field and a mistyped one leave through the same channel.

json_field([("a", JInt(1)), ("b", JInt(2))], "b")
Json.JInt(2)

to_json_string

to_json_string : forall a. (a) -> String

Encode a typed value straight to a canonical JSON string.

to_json_string((1, true))
[1,true]

json_children

json_children : (Json.Json) -> List(Json.Json)

The immediate Json children of a value: an array’s elements, an object’s field values in field order, and nothing at a scalar.

json_children(JObj([("x", JInt(1)), ("ok", JBool(true))]))
[Json.JInt(1), Json.JBool(true)]

json_rebuild

json_rebuild : (Json.Json, List(Json.Json)) -> Json.Json

Put a replacement child list back, in json_children order, keeping an object’s field names. Fails closed: a list of the wrong length yields the value unchanged rather than a truncated or padded one.

json_rebuild(JObj([("x", JInt(1))]), [JInt(2)])
Json.JObj([(x, Json.JInt(2))])

json_layer

json_layer : () -> Control.Layer.Layer(Json.Json)

The children-and-rebuild pair for JSON, so every strategy in Control.Rewrite and every query in Control.Layer works on a decoded document.