std.json
import std.json
json.encode(value) # value -> string
json.decode(text) # string -> value
json.decode(text, decimal: true) # ...with every non-integer as an exact decimal
json.read(path) # a file -> value (takes the same options)
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, written exactly: 12.50m is 12.50 |
bytes | base64 string |
list, map | array, object |
struct | object with its fields, plus "__type__" naming the variant |
| a function, task, channel, stream, module or range | refused: { kind: "bug" } |
A value with no JSON form is refused rather than written as its printed form, so a function that ends up in a payload fails at the json.encode, not at whoever reads "fn(x)" later. http.json and an HTTP request's json: body refuse it the same way.
Exact decimals: decimal: true
A decimal is written exactly, but by default a number decodes as an int or a float, so a money field that goes out as 19.99 comes back as a float. Pass decimal: true and every number that is not an integer decodes as an exact decimal, read from its own text:
import std.json
order = { total: 19.99m, fee: 0.30m }
back = json.decode(json.encode(order), decimal: true)
print(back == order) # true
print(back.fee) # 0.30 - the scale survives too
Integers stay int. A number with more than 28 significant digits does not fit a decimal and is a parse error rather than a rounded value. Use the option for anything carrying money; leave it off for coordinates, ratios and other data you will do float arithmetic with, since a decimal and a float never mix.
Two edges worth knowing, both because JSON has no such type:
bytesencodes as base64 and decodes as a string. Decode it back explicitly withencoding.base64_decode.- A struct decodes as a plain map,
"__type__"key included. Rebuild the record from its fields if you need the type back.
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:
getfor expected absence:get(data, "email").match, which tests rather than accesses, so 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.