Skip to content
Kavachकवच

JSON

Also known as: parse serialize deserialize api response decode encode marshal schema validation jsonc comments trailing comma config file json5

● Intermediate6 minBefore this: Records, UnionsSpec 2.12

JSON is text that carries data between programs. Kavach reads it straight into your own types, or into a Json value when its shape is known only while the program runs.

use std.json

type Order {
    id: Int
    total: Float
}

fn main() ! Error {
    const text = "[\{\"id\": 1, \"total\": 9.5}, \{\"id\": 2, \"total\": 3.0}]"
    const orders = try json.decode<[Order]>(text)
    for o in orders {
        print("order {o.id}: {o.total}")
    }
}

The program ran and printed:

kavach run decode.kav

order 1: 9.5order 2: 3.0
decode.kav: Kavach code, then what running it prints

json.decode<[Order]> reads the text straight into a list of Order records, checking every field's type as it goes.

Two ways to read JSON

WhenUseYou get
You know its shapejson.decode<T>(text)a T, every field checked by the compiler where you use it
You don't, or only partlyjson.parse(text)a Json value, read field by field: see Json values

JSON's datatypes in Kavach

JSONExampleJson caseRead withDecodes into
nullnullNullis_null()none, in an optional (T?)
true / falsetrueBoolean(value)bool()Bool
a number42, 2.5, 1e3Number(digits)int(), float(), decimal(), bigint()Int, Float, Decimal, BigInt, or a sized integer
a string"Asha"Text(value)text()String (Char for one character)
an array[1, 2]Array(items)list()a list, set or tuple
an object{"id": 7}Object(fields)object()a record, or a map with String keys

Every function

FunctionGives
json.parse(text) ! JsonErrorThe Json value written in text, which must be standard JSON (RFC 8259)
json.parse(text, comments: true) ! JsonErrorThe same, also taking // and /* */ comments and a trailing comma in arrays and objects (JSONC, as configuration files are written)
json.decode<T>(text) ! JsonErrorA T read straight from text, with no Json in between
json.decode_value<T>(j) ! JsonErrorA T read from the Json value j
json.decode<T>(text, unknown: JsonUnknown.Ignore)The same, skipping fields T doesn't have (decode_value takes it too)
json.of(value)value as a Json
json.stringify(value, indent: 0)JSON text for a Json or any value that can be encoded; indent above 0 puts each field on its own line
json.schema<T>()The JSON Schema of T as a Json: what ask<T> sends a model (8.4), for modules that work from a type's shape (14.14)
json.describe(f) -> FnSchemaThe function f described: its name, doc comment, parameters with their schemas, the schemas of its result and error, the capabilities it uses, and its example clauses as text

A type's schema

json.schema<T>() gives the JSON Schema of a type, as a Json value. A type's doc comment (///) becomes its schema's "description", and so does a field's:

use std.json

/// A place on the map.
type Place {
    /// How far east, in metres.
    x: Int
    y: Int
}

fn main() {
    const s = json.schema<Place>()
    print(s.get("description"))
    print(s.get("properties").get("x"))
    print(s.get("properties").get("y"))
}

The program ran and printed:

kavach run schema-docs.kav

"A place on the map."{"type":"integer","description":"How far east, in metres."}{"type":"integer"}
schema-docs.kav: Kavach code, then what running it prints

When both a field and its type have one, the field's comes first in one description, then the type's:

use std.json

/// How sure.
type Level = Low | High

type Guess {
    /// How sure the model is.
    level: Level
}

fn main() {
    const level = json.schema<Guess>().get("properties").get("level")
    print(level.get("description"))
}

The program ran and printed:

kavach run schema-docs-both.kav

"How sure the model is. How sure."
schema-docs-both.kav: Kavach code, then what running it prints

A function described

json.describe(f) describes a function from its signature, when the program is compiled: its name, doc comment, parameters with their schemas, the schemas of its result and error, what it uses, and its example clauses as text. A web framework uses it, with json.schema<T>(), to write an OpenAPI description of its handlers.

use std.json

type NewOrder {
    item: String
    qty: Int
}

type ShopError = OutOfStock(item: String) | TooMany(qty: Int)

/// Places an order and
/// gives its number.
fn place(o: NewOrder) -> Int ! ShopError uses Console {
    if o.qty > 10 {
        fail ShopError.TooMany(qty: o.qty)
    }
    print("placing {o.item}")
    1
}

/// The price of `qty` pens.
fn price(qty: Int) -> Int
    example price(3) == 30
{
    qty * 10
}

fn main() {
    const d = json.describe(place)
    print(d.name)
    print(d.doc)
    print(d.params[0].name)
    print(d.params[0].schema.get("required"))
    print(d.result)
    print(d.capabilities)
    print(json.describe(price).examples)
}

The program ran and printed:

kavach run describe.kav

placePlaces an order and gives its number.o["item","qty"]{"type":"integer"}["Console"]["price(3) == 30"]
describe.kav: Kavach code, then what running it prints
pub type FnSchema {
    pub name: String
    pub doc: String               // the `///` comment, its lines joined by spaces; "" when it has none
    pub params: [ParamSchema]
    pub result: Json              // the schema of what it returns; {"type": "null"} for nothing
    pub error: Json               // the schema of its error; null when it can't fail
    pub capabilities: [String]    // what it `uses`, written or worked out (8.1)
    pub examples: [String]        // each `example` clause as written (7.6)
}

pub type ParamSchema {
    pub name: String
    pub schema: Json
}
syntax: Kavach code
  • When f names a function, the result is a constant FnSchema, so reading it costs nothing more than reading any value.
  • A parameter, result or error whose type has no schema, such as a function, is a compile error.

Given a function value instead, such as a parameter or an item of a list of handlers, json.describe(value) gives FnSchema?: the description of the named function the value is, or none for a lambda (and for a generic function, which has no one description). It works inside a generic function too, with the types it was called with. There, a parameter, result or error with no schema (a function, a record holding one, the built-in Error) is {}, any value, so every named function can be described. So a framework handed its routes as values can describe each one:

use std.json

type Order {
    id: Int
}

/// Lists the orders.
fn list_orders() -> [Order] {
    [Order(id: 1)]
}

/// Lists the orders not yet paid.
fn unpaid() -> [Order] {
    []
}

fn summary(handler: fn() -> [Order]) -> String {
    if let d = json.describe(handler) {
        return "{d.name}: {d.doc}"
    }
    "no description"
}

fn main() {
    const routes = [list_orders, unpaid, () => []]
    for r in routes {
        print(summary(r))
    }
}

The program ran and printed:

kavach run describe-value.kav

list_orders: Lists the orders.unpaid: Lists the orders not yet paid.no description
describe-value.kav: Kavach code, then what running it prints

Safe with hostile input

Text from outside can be built to do harm. Parsing has limits, and breaking one is an error, not a crash:

use std.json

fn main() {
    const text = "[".repeat(200) + "]".repeat(200)
    const doc = json.parse(text) catch e => {
        print("refused: {e}")
        return
    }
    print(doc.len())
}

The program ran and printed:

kavach run deep.kav

refused: TooDeep(limit: 128)
deep.kav: Kavach code, then what running it prints

A key written twice could mean two different things to two programs, so it is refused:

use std.json

fn main() {
    const text = "\{\"amount\": 10, \"amount\": 10000}"
    const doc = json.parse(text) catch e => {
        print("refused: {e}")
        return
    }
    print(doc)
}

The program ran and printed:

kavach run duplicate.kav

refused: DuplicateKey(key: "amount", line: 1, column: 16)
duplicate.kav: Kavach code, then what running it prints

A number too big for its field is an error too, never rounded quietly. The limits on depth, size and fields can be raised for one call, as in json.parse(body, max_depth: 1_000):

WhatLimit or sizeSource
Nesting128 levels deep; max_depth raises it, up to 10,00012-json.md
Text64 MB; max_bytes raises it12-json.md
Fields in one object100,000; max_fields raises it12-json.md
A number's exponentfrom 10^-10,000 to 10^10,000, for decimal() and bigint()12-json.md

Every limit in Kavach, on one page: Limits and Sizes.

Comments, for configuration files

json.parse reads standard JSON (RFC 8259). Configuration files are often written with comments and a trailing comma (JSONC): comments: true takes them, and only when asked.

use std.json

fn main() ! Error {
    const text = "\{\n  // where the server listens\n  \"port\": 8080,\n  \"hosts\": [\"a\", \"b\",],\n}"
    const settings = try json.parse(text, comments: true)
    print(settings["port"].int() ?? 0)
    const _ = json.parse(text) catch e => {
        print("plain parse: {e}")
        Json.Null
    }
}

The program ran and printed:

kavach run comments.kav

8080plain parse: Syntax(line: 2, column: 3, message: "expected a key in quotes")
comments.kav: Kavach code, then what running it prints

NaN, Infinity, hex numbers and the rest of JSON5 are syntax errors, even with comments: true.

One page for each form

Go deeper the why, for readers who want it

How fast a whole JSON program runs, measured with the steps to repeat it below:

300,000 orders (52 MB): write, parse, read, decode · WSL Ubuntu 24.04 · i7-13700HX · kavach build (LLVM) · Python 3.12 (its json module is C) · typical of 5 runs

Kavach851 ms · finished 1st
Python 3.125,270 ms · 6.2× behind

And each step on its own:

decode — the text straight into records · WSL Ubuntu 24.04 · i7-13700HX · kavach build (LLVM) · Python 3.12 (its json module is C) · typical of 5 runs

Kavach294 ms · finished 1st
Python 3.122,282 ms · 7.8× behind

stringify — every order to JSON text · WSL Ubuntu 24.04 · i7-13700HX · kavach build (LLVM) · Python 3.12 (its json module is C) · typical of 5 runs

Kavach189 ms · finished 1st
Python 3.121,244 ms · 6.6× behind

parse — the text back into dynamic values · WSL Ubuntu 24.04 · i7-13700HX · kavach build (LLVM) · Python 3.12 (its json module is C) · typical of 5 runs

Kavach176 ms · finished 1st
Python 3.12578 ms · 3.3× behind

read — qty and price of 1.5M items from parsed values · WSL Ubuntu 24.04 · i7-13700HX · kavach build (LLVM) · Python 3.12 (its json module is C) · typical of 5 runs

Kavach91 ms · 1.8× behind
Python 3.1252 ms · finished 1st

Since decision 0071, a Json number keeps its exact digits, so int() and float() read the digits each time — but a profile showed that is only about 5% of reading. And since decision 0082, a parsed document is one list of words: item["qty"].int() finds the field on the tape, where a constant key remembers its place, and reads the digits there, making nothing. Most of what reading still loses to Python is the small object made for each array item a loop visits.

Verify itgit clone https://github.com/Mithlesh04/kavach && cd kavach kavach build benchmarks/json/kavach/json.kav && ./json; python3 benchmarks/json/python/json_bench.py