std.json
import std.json
json.encode(value) # value -> string
json.decode(text) # string -> value
json.read(path) # a file -> value
json.write(path, value)
The globals json_encode and json_decode do the same encoding and decoding without an import; this module adds the file forms.
Deterministic output
Maps encode with sorted keys. The same value always produces the same bytes, so encoded JSON can be compared, hashed, checked into a repository, or asserted on in a test without normalizing it first.
Type mapping
| Ecko | JSON |
|---|---|
null, bool, string | as-is |
int, float | number |
decimal | number in canonical text - round trip is float-precision |
bytes | base64 string |
list, map | array, object |
struct | object |
Two lossy edges worth knowing, both because JSON has no such type:
decimalencodes as a number and decodes as a float. If exactness must
survive transport, send a string and rebuild with decimal("..."). See Numbers.
bytesencodes as base64 and decodes as a string. Decode it back
explicitly with encoding.base64_decode.
Errors
Malformed input raises { kind: "parse", format: "json" }, and json.read adds path. Always catch around anything decoded from outside:
try {
data = json.decode(body)
} catch (e) {
match get(e, "kind") { "parse" => bad_request() _ => error(e) }
}
Decoded data is uncertain in shape
A decoded value is a map whose keys you hope are there. data.user.email raises if any level is missing. Two better tools:
a missing key is a non-match rather than an error.
For validating a whole payload with all errors collected, use the validate package.
Typed model output
ai[json<List<Int>>] asks a model for a shaped JSON value and coerces the reply through the schema, which is usually better than decoding a string yourself - see typed output.