Module Json

JSON encoding/decoding.

type Json = recursive either {
  .bool Bool,
  .list List<self>,
  .null!,
  .number Float,
  .object BoxMap.Readonly<String, self>,
  .string String,
}

A materialized JSON value.

Notes:

  • .number uses Float.
  • Encoding .number(Float.NaN), .number(Float.Inf), or .number(Float.NegInf) produces JSON null.
  • Object keys are normalized through BoxMap.Readonly, so duplicate source keys and original key order are not preserved.
type Json.Error = String

Human-readable JSON parse/decode errors.

Errors from formats start with the path to the offending value, e.g. .users[3].name: expected a string, found a number.

type Json.Format<a> = box choice {
  .parse(Json) => Try<Json.Error, a>,
}

A reusable parser from a materialized Json value into a typed value.

A Format<a> exposes a single .parse operation that returns .ok value on success, or .err error saying where and why the value didn't match.

dec Json.And : [<a: drop> Json.ObjectFormat<a>, <b: drop> Json.ObjectFormat<b>] Json.ObjectFormat<(b) a>

Runs two object formats against the same object and returns both results.

The result shape is (right) left, so Json.Field("age", Json.Number)->Json.And(Json.Field("name", Json.String)) returns (String) Float.

Both object formats return safely discardable values because either side may need to be discarded if the other side fails.

dec Json.Empty : Json.ObjectFormat<!>

Parses an empty object. Ignores any fields, does not fail if fields are present.

dec Json.Equals : [Json, Json] Bool

Tests two Json values for structural equality.

Object key order does not matter, because objects are normalized through BoxMap.Readonly.

dec Json.Field : [String, <a> Json.Format<a>] Json.ObjectFormat<a>

Parses a required object field with the given format.

For example, Json.Field("name", Json.String) accepts an object whose "name" field is a JSON string.

dec Json.Int : Json.Format<Int>

Parses a JSON number that is an integer. Numbers with a fractional part are rejected.

dec Json.List : [<a: drop> Json.Format<a>] Json.Format<List<a>>

Parses a JSON array with the given item format.

The item type must be safely discardable because earlier parsed items may need to be discarded if a later item fails to parse.

dec Json.Map : [<a> Json.Format<a>, <b> box [a] b] Json.Format<b>

Applies a pure mapping function to a successful parse result.

Use Json.Map when parsing cannot fail once the underlying format succeeds. Use Json.Then for fallible post-processing.

dec Json.Nat : Json.Format<Nat>

Parses a JSON number that is a non-negative integer.

dec Json.Parse : [String, <a> Json.Format<a>] Try<Json.Error, a>

Decodes JSON text and parses it with a format in one step.

Json.Parse(`{"name": "Par"}`, Json.Object(Json.Field("name", Json.String)))
// = .ok "Par"
dec Json.Tagged : [String, <a> List<(Json, <b> Json.ObjectFormat<b>) box [b] a>] Json.Format<a>

Parses tagged JSON objects by matching one field against literal JSON tags.

The first argument selects the field to inspect. Each branch provides a tag literal, an object format, and a mapper into the result type. Tags are compared with Json.Equals, so they may be strings, numbers, booleans, or null, not just strings.

type Command = either {
  .sleep Float,
  .say String,
}

def CommandFormat = Json.Tagged("operation", *(
  (.string "sleep", Json.Field("seconds", Json.Number)) box [seconds] .sleep seconds,
  (.string "say", Json.Field("message", Json.String)) box [message] .say message,
))
dec Json.Then : [<a> Json.Format<a>, <b> box [a] Try<Json.Error, b>] Json.Format<b>

Applies a fallible post-processing step to a successful parse result.

The mapping function returns .ok value to accept the parsed result or .err error to reject it with a message.

dec Json.Union : [<a> List<(<b> Json.Format<b>) box [b] a>] Json.Format<a>

Tries multiple formats in order and returns the first successful result.

Each branch may parse to its own intermediate type before mapping into the shared result type.

type Answer = either {
  .yes!,
  .text String,
}

def AnswerFormat = Json.Union(*(
  (Json.Literal(.string "yes")) box [!] .yes!,
  (Json.String) box [text] .text text,
))