JSON
Decode, inspect, and encode JSON.
On this page
valk.json decodes JSON into your own types or into a dynamic json.Value, and encodes any value back to JSON.
Decode into a type#
use valk.json
class Profile {
name: String
age: int
email: ?String
active: bool (true)
tags: Array[String]
}
fn main() {
let text = "{\"name\":\"Ada\",\"age\":36,\"tags\":[\"admin\"],\"extra\":1}"
let profile = json.decode_to[Profile](text) ! {
println("Not a valid profile: " + E.code)
return
}
println(profile.name) // Ada
println(profile.active) // true
println(isset(profile.email)) // false
println(profile.tags.length) // 1
}
Every field must be present unless it is nullable (?String) or has a default (value). Unknown members are skipped. Nested classes, arrays, maps and time.DateTime (as ISO 8601 text) work as field types.
Integer fields need a whole number (3.0 reads as 3), float fields take any number. A bad type throws wrong_type, a missing field missing, and malformed JSON invalid. Decoding checks the shape only; check rules such as "not empty" yourself or with validation.
Read a document without a type#
json.decode returns a json.Value you can read and change. Index with [key] for objects and [index] for arrays:
use valk.json
fn main() {
let doc = json.decode("{\"name\":\"Ada\",\"langs\":[\"valk\",\"c\"],\"visits\":3}") ! {
println("Malformed JSON")
return
}
println(doc["name"].string) // Ada
println(doc["langs"][1].string) // c
println(doc["visits"].int + 1) // 4
println(doc["missing"].is_null) // true
each doc["langs"].array as lang {
println(lang.string)
}
}
.string, .int, .float, .bool, .array and .object convert the value and fall back to an empty one when it has another type. To require a type, use the methods that throw:
| Method | Throws when |
|---|---|
get_required(key) |
The key or index is missing |
get_string(key), get_int(key), get_float(key), get_bool(key) |
Missing, or another type |
get_array(key), get_object(key) |
Missing, or another type |
string_value(), int_value(), ... |
The value itself has another type |
has(key) and has_string(key) and friends check without throwing, and kind is a json.Kind (null, string, bool, int, float, array, object).
Change a document#
use valk.json
fn main() {
let doc = json.decode("{\"name\":\"Ada\",\"tags\":[]}") ! return
doc.set("name", "Sam")
doc.set_bool("active", true)
doc["tags"].append_string("new")
doc.remove("name")
println(doc.encode()) // {"tags":["new"],"active":true}
}
set_int, set_float, set_string, set_null and append_* exist for every kind. json.from(value) turns any value into a json.Value, and doc.to_type[Profile]() turns one into a class with the same rules as decode_to.
Read large documents with a view#
json.read returns a read-only json.View. It reads the same way as a Value but costs less to build, which suits large documents you only read:
use valk.json
fn main() {
let data = json.read("{\"users\":[{\"name\":\"Ada\"},{\"name\":\"Bo\"}],\"settings\":{\"theme\":\"dark\"}}") ! return
each data["users"] as user {
println(user["name"].string)
}
each data["settings"] as value, key {
println(key.string + " = " + value.string) // theme = dark
}
let editable = data["settings"].to_value() // a json.Value copy
editable.set("theme", "light")
}
Which to use:
json.decode_to[T]when you know the shape.json.readwhen you only read and the shape varies, or the document is large.json.decodewhen you need to change it.
Encode#
use valk.json
class Point {
x: int
y: int
}
fn main() {
let points = Array[Point]{ Point { x: 1, y: 2 } }
println(json.encode(points)) // [{"x":1,"y":2}]
println(json.encode(Map[int]{ "a" => 1 })) // {"a":1}
println(json.encode(points, true)) // indented with four spaces
}
json.encode takes classes, structs, arrays, maps, numbers, strings, bools, null and json.Value. A nullable field that is null is written as null. A whole float keeps its .0. json.encode_into(value, writer) writes straight to a file, socket or ByteBuffer. Encode a small response class rather than an internal object, so you choose which fields leave the program.
Limits#
decode, decode_to and read take optional limits against hostile input:
| Argument | Default | Error |
|---|---|---|
max_depth |
128 | too_deep |
max_bytes |
64 MiB | too_large |
max_entries |
1,000,000 | too_large |
use valk.json
fn main() {
let doc = json.decode("[[[1]]]", max_depth: 2, max_bytes: 1024 * 1024) ! {
println(E.code) // too_deep
return
}
}
A number without a fraction or exponent is an int; a larger one than int holds becomes a float, unless it is decoded into a u64 field.

