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
Jsontree and a totaldecode/encodepair for payloads whose shape you do not control.decodenever partially succeeds: a malformed input is oneJsonErrorcarrying 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 theToJson/FromJsonclasses with instances for the base types and the containers, so a declared type converts to and from aJsontree structurally. (Derivation is a manual typeclass rather thanderiving (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(Intis arbitrary precision, so no integer overflows); a number decodes toJFloatonly 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 underdecode, and accepted, normalized, underdecode_lossy. No silent rounding, no silent1e999-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 aJsonvalue has a well-defined hash and diff, anddecode(encode(v))returnsv(with the one deliberate normalization that an integer-valuedJFloatprints as an integer and decodes back toJInt, 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.