json

Parse JSON strings into typed silt values and serialize values to JSON.

Summary

FunctionSignatureDescription
parse(String, type a) -> Result(a, JsonError)Parse JSON object into record
parse_list(String, type a) -> Result(List(a), JsonError)Parse JSON array into record list
parse_map(String, type a) -> Result(Map(String, a), JsonError)Parse JSON object into map
pretty(a) -> StringPretty-print value as JSON
stringify(a) -> StringSerialize value as compact JSON

Errors

Every fallible json.* call returns Result(T, JsonError). The JsonError enum has four variants you can pattern-match on, or fall back to e.message() when you just want a rendered string (trait Error for JsonError is wired in):

VariantFieldsMeaning
Syntax(msg, offset)String, IntMalformed JSON at offset bytes
TypeMismatch(expected, actual)String, StringA field’s JSON type did not match the target field type
MissingField(name)StringA required (non-Option) field was absent
Other(msg)StringAnything else (out-of-range numbers, internal failures)

See stdlib errors for the shared Error trait.

Decodable types

The type argument of json.parse, json.parse_list and json.parse_map names the type to decode into:

FunctionType argument
json.parseInt, Float, String, Bool, or a record type
json.parse_lista record type
json.parse_mapInt, Float, String, Bool, or a record type

The fields of a record type (and of every record type nested in it) may have these types:

  • Int, Float, String, Bool
  • Date, Time, DateTime
  • List(T), Option(T), Map(String, T)
  • tuples, such as (Int, String)
  • non-generic record types
  • type aliases of any of these

where T is again one of these types. Anything else (a Set, an enum, a generic record, a function, a Map whose key is not String, a type parameter) has no decoder.

A number of the document goes into an Int when it is a whole number in Int’s range (1, 1.0, 1e3). A number with a fraction (1.5), or one out of that range, is Err(TypeMismatch("Int", ...)): nothing is cut off or rounded.

When the type is written at the call, json.parse(text, Config), the compiler checks it. A record with a field no decoder exists for is a compile error that names the field and its type; so is an enum, or a type the function does not take (such as json.parse_list(text, Int)). The check applies equally to a decoder imported by name (import json.{ parse }) and to one called through a module alias. When the type only arrives at run time, through a type a parameter, the same problems are reported then: a field without a decoder gives an Err(Other(msg)) whose message names the field and its record, and a type the function does not take is a run-time error.

Nesting limits

json.stringify and json.pretty write a value nested up to 1,000 levels deep. The element of a list or of a tuple, the field of a record or of an enum variant and the value of a map are each one level further in than what holds them; the content of a Some is not (it is written in the Some’s place). A value nested deeper is a runtime error, as is any value they cannot write:

json.stringify: the value is nested more than 1000 levels deep

The parsers stop sooner: json.parse, json.parse_list and json.parse_map read a document whose arrays and objects are nested up to 127 levels deep, and give a Syntax error (recursion limit exceeded) for one nested deeper. A value written at more levels than that is valid JSON that silt itself does not read back.

json.parse

json.parse(s: String, type a) -> Result(a, JsonError)

Parses a JSON string into a record of type a. The type is passed as a type parameter (see Generics — not a string). Fields are matched by name; Option fields default to None if missing from the JSON.

Fields of type Date, Time, and DateTime (from the time module) are automatically parsed from ISO 8601 strings. DateTime fields also accept timezone-aware formats (RFC 3339) — the offset is applied and the value is stored as UTC:

Field typeAccepted formatsExample
DateYYYY-MM-DD"2024-03-15"
TimeHH:MM:SS, HH:MM"14:30:00"
DateTimeYYYY-MM-DDTHH:MM:SS, with optional Z or ±HH:MM offset"2024-03-15T09:00:00+09:00"
import json

type User {
  name: String,
  age: Int,
}

fn main() {
  let input = """{"name": "Alice", "age": 30}"""
  match json.parse(input, User) {
    Ok(user) -> println(user.name)
    Err(e) -> println("Error: {e.message()}")
  }
}
open in playground

Date/Time example:

import json
import time

type Event {
  name: String,
  date: time.Date,
}

fn main() -> Result(Unit, json.JsonError) {
  let e = json.parse("""{"name": "launch", "date": "2024-03-15"}""", Event)?
  println(e.date |> time.weekday) -- Friday
  Ok(())
}
open in playground

json.parse_list

json.parse_list(s: String, type a) -> Result(List(a), JsonError)

Parses a JSON array where each element is a record of type a.

import json
import list

type Point {
  x: Int,
  y: Int,
}

fn main() {
  let input = """[{"x": 1, "y": 2}, {"x": 3, "y": 4}]"""
  match json.parse_list(input, Point) {
    Ok(points) -> list.each(points) { p -> println("{p.x}, {p.y}") }
    Err(e) -> println("Error: {e.message()}")
  }
}
open in playground

json.parse_map

json.parse_map(s: String, type a) -> Result(Map(String, a), JsonError)

Parses a JSON object into a Map(String, v). The type is passed as a type parameter (Int, Float, String, Bool, or a record type).

import json
import map

fn main() {
  let input = """{"x": 10, "y": 20}"""
  match json.parse_map(input, Int) {
    Ok(m) -> println(map.get(m, "x")) -- Some(10)
    Err(e) -> println("Error: {e.message()}")
  }
}
open in playground

json.pretty

json.pretty(value: a) -> String

Serializes a value to a pretty-printed JSON string (with indentation and newlines). It writes and refuses the same values as json.stringify.

import json

fn main() {
  let data = #{ "name": "silt", "version": "1.0" }
  println(json.pretty(data))
}
open in playground

json.stringify

json.stringify(value: a) -> String

Serializes a value to a compact JSON string. A map is written as an object; a key of it that is no String is written as its shown text (so the Float key 1.0 reads "1.0").

json.stringify and json.pretty write the same values. A value JSON has no form for is a runtime error that names its kind, wherever in the value it is: Bytes, a Set, a function (the constructor of a variant, such as Some, is one), a channel, a task handle, a TCP listener or stream.

json.stringify: unsupported value kind Bytes

To send bytes as JSON, turn them into text first (bytes.to_string, bytes.to_hex or bytes.to_base64); to send a set, make a list of it (set.to_list).

import json

fn main() {
  let data = #{ "key": [1, 2, 3] }
  println(json.stringify(data))
  -- {"key":[1,2,3]}
  println(json.stringify(#{ 1.0: "one" })) -- {"1.0":"one"}
}
open in playground