Docs/Data & text

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#

Valk
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:

Valk
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#

Valk
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:

Valk
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.read when you only read and the shape varies, or the document is large.
  • json.decode when you need to change it.

Encode#

Valk
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
Valk
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.