Module Json

JSON encoding/decoding.

type Json.Error = String

Human-readable JSON parse/decode errors.

type Json.Format<a> = box choice {
  .parse => [Json] Option<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! on mismatch.

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.ObjectFormat<a> = box choice {
  .parseObject => [BoxMap.Readonly<String, Json>] Option<a>,
}

A reusable parser specialized to JSON objects.

Use Json.Object(...) to lift an ObjectFormat<a> into a full Format<a>.

dec Json.And : <a: box>[Json.ObjectFormat<a>] <b: box>[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 non-linear values because either side may need to be discarded if the other side fails.

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.List : <a: box>[Json.Format<a>] Json.Format<List<a>>

Parses a JSON array with the given item format.

The item type must be non-linear because earlier parsed items may need to be discarded if a later item fails to parse.

dec Json.Map : <a>[Json.Format<a>] [type 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.Tagged : [type a, String, 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(type Command, "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>] [type b, box [a] Option<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! to reject it.

dec Json.Union : [type 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(type Answer, *(
  (Json.Literal(.string "yes")) box [!] .yes!,
  (Json.String) box [text] .text text,
))