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
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
| When | Use | You get |
|---|---|---|
| You know its shape | json.decode<T>(text) | a T, every field checked by the compiler where you use it |
| You don't, or only partly | json.parse(text) | a Json value, read field by field: see Json values |
JSON's datatypes in Kavach
| JSON | Example | Json case | Read with | Decodes into |
|---|---|---|---|---|
| null | null | Null | is_null() | none, in an optional (T?) |
| true / false | true | Boolean(value) | bool() | Bool |
| a number | 42, 2.5, 1e3 | Number(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
| Function | Gives |
|---|---|
json.parse(text) ! JsonError | The Json value written in text, which must be standard JSON (RFC 8259) |
json.parse(text, comments: true) ! JsonError | The same, also taking // and /* */ comments and a trailing comma in arrays and objects (JSONC, as configuration files are written) |
json.decode<T>(text) ! JsonError | A T read straight from text, with no Json in between |
json.decode_value<T>(j) ! JsonError | A 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) -> FnSchema | The 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"}
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."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"]
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
}- When
fnames a function, the result is a constantFnSchema, 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
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)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)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):
| What | Limit or size | Source |
|---|---|---|
Nesting | 128 levels deep; max_depth raises it, up to 10,000 | 12-json.md |
Text | 64 MB; max_bytes raises it | 12-json.md |
Fields in one object | 100,000; max_fields raises it | 12-json.md |
A number's exponent | from 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")
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
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
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
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
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
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.
git clone https://github.com/Mithlesh04/kavach && cd kavach
kavach build benchmarks/json/kavach/json.kav && ./json; python3 benchmarks/json/python/json_bench.py