Modules

Json

moduleextensionSource

Available on macOS, Linux, Windows, Wasm

#import "Extensions/Json";

JSON to and from your own types. to_json encodes a value and from_json decodes text into a type; both are generated from the type at compile time. When the shape is not known, decode into a Json_Value instead. Behaviour per field is set with @json(...) notes.

Example↗
#import "Extensions/Json";

User :: struct @json(rename_all=camelCase) {
    id: int;
    display_name: string;                 // "displayName"
    email: string;      @json(skip_serializing_if=empty)
    tags: [..] string;
}

ada := User.{id = 7, display_name = "Ada"};
text := to_json(ada);                     // pretty: to_json(ada, pretty = true)
user, error := from_json(User, text);     // error.kind == .NONE on success
value, value_error := from_json(Json_Value, text);    // when the shape is unknown
free_json(*user);                         // also for a Json_Value and a Json_Error

Procedures#

General#

(error: Json_Error) -> string

The error's path in jsonpath form, as jsonb_path_query takes it: $ for the document itself, $.address.tags[2], and a key that is not a plain identifier quoted: $."we ird". Allocated with the temporary allocator.

Item :: struct { tags: [..] string; }

_, error := from_json(Item, "{\"tags\": [\"a\", 2]}");
assert(json_path_text(error) == "$.tags[1]");
free_json(*error);
↗
(value: $T, pretty := false) -> string

Encodes value. Output is compact unless pretty (two-space indent). The result comes from context.allocator.

Point :: struct { x: int; y: int; }

text := to_json(Point.{ 1, 2 });
assert(text == "{\"x\":1,\"y\":2}");
free(text);
↗
(builder: *String_Builder, value: $T, pretty := false)

Like to_json, appending to a String_Builder instead of allocating a string.

builder: String_Builder;
write_json(*builder, s32.[1, 2, 3]);
assert(builder_to_string(*builder,, temp) == "[1,2,3]");
↗
from_json2 overloadsSource
($T: Type, source: Json_Source, options := Json_Options.{}) -> (T, Json_Error)
($T: Type, text: string, options := Json_Options.{}) -> (T, Json_Error)

Decodes a T from a source in pieces. The pieces are gathered in one buffer (the decoder wants the whole document, and its strings are copied out of it, so the buffer is freed before this returns) and then decoded exactly as from_json(T, text) would, so a document split at any byte gives the same value or the same error. A source that fails gives a .READ error with the source's text; a document longer than options.max_size is a .LIMIT error and the rest is not read.

Chunks :: struct { parts: [] string; next: int; }

fill :: (data: *void, buffer: [] u8) -> s64, string {
    chunks := cast(*Chunks) data;
    if chunks.next >= chunks.parts.count return 0, "";
    part := chunks.parts[chunks.next];
    chunks.next += 1;
    memcpy(buffer.data, part.data, part.count);
    return part.count, "";
}

chunks := Chunks.{ parts = .["[1, ", "2, 3]"] };
numbers, error := from_json([] s64, Json_Source.{ *chunks, fill });
assert(error.kind == .NONE && numbers.count == 3);
free_json(*numbers);
↗
(w: *Json_Writer, value: *$T)

Writes the JSON of the value value points to to the writer w. This and json_decode are what with= handlers call for the parts they do not encode themselves.

Price :: struct {
    cents: s64; @json(with=dollars)

    dollars_encode :: (w: *Json_Writer, value: *s64) {
        json_write_float64(w, cast(float64) value.* / 100.0);
    }

    dollars_decode :: (r: *Json_Reader, out: *s64) -> bool {
        dollars: float64;
        if !json_read_float64(r, *dollars) return false;
        out.* = cast(s64) (dollars * 100.0 + 0.5);
        return true;
    }
}

assert(to_json(Price.{ 1250 }) == "{\"cents\":12.5}");
price, _ := from_json(Price, "{\"cents\": 3.25}");
assert(price.cents == 325);
↗

Value#

(value: *Json_Value) -> (string, bool)

The text and true, or "" and false when the value is not a string. The text belongs to the value.

Writer#

(builder: *String_Builder, text: string)

Appends text as a JSON string with its quotes. Control characters, quotes and backslashes are escaped; bytes that are not valid UTF-8 become U+FFFD so the output is always valid JSON.

(w: *Json_Writer, first: *bool, quoted_key: string)

Starts a member: the separator, then the key. quoted_key is the key with its quotes and escapes already applied (the generated code does that at compile time).

(w: *Json_Writer, first: *bool, quoted_key: string, first_form: string, next_form: string)

json_key with the compact forms prebuilt at compile time: first_form is "key": and next_form is ,"key":, so compact output costs one append.

Reader#

(r: *Json_Reader, key: string)

Say the value being read is at key / index of its parent. The generated decoders call these while an error travels up, so the error's path reads address.tags[2].

(r: *Json_Reader, first: *bool) -> (string, Object_Status)

Reads up to the next member of the object: its key (valid until the next read) and the colon after it, or the closing brace. first is true before the first call.

(r: *Json_Reader) -> (string, bool)

A string value, copied into new memory from context.allocator (with borrow, a slice of the input when it has no escapes).

(r: *Json_Reader) -> (string, bool)

A string value without copying: a view of the input, or of the reader's scratch space when it has escapes. Valid until the next read; copy it to keep it.

(r: *Json_Reader, key: string) -> string

Keeps a key from json_read_name: a slice of the input with borrow (unless it had escapes and lives in the scratch space), else a copy.

(r: *Json_Reader, out: *s64, low: s64, high: s64, type_name: string) -> bool

Reads an integer into out, which must fit between low and high. type_name appears in the message when it does not.

Query#

($T: Type, pairs: [] Query_Pair, options := Json_Options.{}) -> (T, Json_Error)

Decodes a T from pairs. A key that repeats gives an array for an array field, and is a "duplicate field" error for any other. Keys no field has are ignored unless T is marked deny_unknown_fields. Everything allocated (strings, arrays) comes from context.allocator and is released by free_json, as for from_json.

Search :: struct @json(default) { q: string; limit := 20; tags: [..] string; }

// The pairs of "?q=jai&tags=a&tags=b".
pairs := Query_Pair.[.{"q", "jai"}, .{"tags", "a"}, .{"tags", "b"}];
search, error := from_query(Search, pairs);

assert(error.kind == .NONE);
assert(search.q == "jai");
assert(search.limit == 20);
assert(search.tags.count == 2);
↗
($T: Type, text: string, options := Json_Options.{}) -> (T, Json_Error)

Decodes a T from one piece of text: an integer, float, bool, string or enum. from_param(int, "42") is 42; from_param(Color, "red") the member red; a string is taken as it is.

number, error := from_param(int, "42");
assert(error.kind == .NONE && number == 42);

_, error = from_param(int, "forty-two");
assert(error.kind != .NONE);
free_json(*error);
↗

Types#

:: enum

Why decoding failed.

NONE
SYNTAX
not JSON
TYPE
valid JSON of the wrong shape: a string where a number belongs
OUT_OF_RANGE
a number that does not fit its type
INVALID_VALUE
an enum name or value the type does not have
MISSING_FIELD
UNKNOWN_FIELD
with deny_unknown_fields
DUPLICATE_FIELD
LIMIT
too deep, or too large
READ
the source of a stream failed before the document ended
UNKNOWN_VARIANT
a tag that names none of a Variant's cases
:: struct

Why decoding failed and where. path names the value inside the document (tags[2], address.zip); line and column count from 1, the column in characters. message and path are allocated with context.allocator: free_json releases them.

message: string
offset: int
byte offset in the text
line: int
column: int
path: string
_, error := from_json([] s64, "[1, 2");
assert(error.kind == .SYNTAX && error.line == 1);
free_json(*error);
↗
:: enum

What a repeated key in an object does; see Json_Options.

ERROR
a repeated key in an object is a .DUPLICATE_FIELD error
LAST
the last one wins (what Postgres jsonb keeps); the earlier value is released
:: struct

Limits and strictness for from_json.

max_depth: := 256
nesting of arrays and objects
max_size: := 1 << 30
bytes of text
deny_unknown_fields: := false
Every struct denies unknown keys, as if it had @json(deny_unknown_fields). A struct's own note still denies when this is false.
missing_fields_default: := false
A missing field that is not optional keeps its declared initializer instead of failing with .MISSING_FIELD.
duplicate_keys: := Json_Duplicate_Keys.ERROR
What a repeated key in an object does. A Hash_Table already keeps the last value.
borrow: := false
Strings without escapes (and the keys and numbers of a Json_Value) are slices of the input text instead of copies; strings with escapes are still allocated from context.allocator. The input must outlive the result, and the result must never be released with free_json (nor may a failed decode free through a heap allocator): decode with context.allocator set to an arena or temp, whose free does nothing, and reset that instead. Ignored by the streaming from_json and by from_query/from_param, whose text does not outlive the call.
Point :: struct { x: int; y: int; }

options: Json_Options;
options.deny_unknown_fields = true;
_, error := from_json(Point, "{\"x\": 1, \"y\": 2, \"z\": 3}", options);
assert(error.kind == .UNKNOWN_FIELD);
free_json(*error);
↗
Json_Presenceenumsince 0.27.0Source
:: enum

A field that distinguishes a key that is missing, a null and a value. As a struct member a missing key leaves .MISSING (never a missing-field error), null gives .NULL and any other value decodes into value and gives .SET. Encoding omits the key for .MISSING, writes null for .NULL and the value for .SET. Outside a struct member (the document, an array element) .MISSING encodes as null. free_json releases value when it is .SET.

MISSING
NULL
SET
Json_Maybestructsince 0.27.0Source
:: struct

A member type for a field that may be absent: it holds a Json_Presence and the value.

value: T
Patch :: struct { name: Json_Maybe(string); }

missing, _ := from_json(Patch, "{}");
assert(missing.name.state == .MISSING);

cleared, _ := from_json(Patch, "{\"name\": null}");
assert(cleared.name.state == .NULL);

renamed, _ := from_json(Patch, "{\"name\": \"Ada\"}");
assert(renamed.name.state == .SET && renamed.name.value == "Ada");
free_json(*renamed);
↗
:: struct

How the structural index fared since the program began, for tests and curiosity.

parsed: int
texts decoded with an index
unindexable: int
texts stage 1 declined (control characters in strings, bad UTF-8...)
sparse: int
large texts whose strings are too short or too escaped for the index to pay
reparsed: int
texts that failed with an index and were parsed again to get the error
:: #type (data: *void, buffer: [] u8) -> s64, string

Where a document comes from when it is not in memory yet: fill puts the next piece of text in buffer and returns how many bytes it wrote (0 at the end), and a non-empty string when the source failed (what went wrong). data is handed to it as it is. Json does not know what the pieces come from; Io.read_json makes one from an Io.Reader.

:: struct

One JSON value. Only the fields of its kind are used: BOOL boolean NUMBER text, the number as written (exact for integers of any size; see json_as_s64) STRING text, the contents with escapes resolved ARRAY items OBJECT members, in document order (duplicate keys are kept; json_get finds the last) Everything a Json_Value owns comes from context.allocator; free_json releases it.

kind: Json_Kind
boolean: bool
text: string
items: [..] Json_Value
members: [..] Json_Member
doc := json_object();
json_set(*doc, "name", json_string("Ada"));
tags := json_array();
json_push(*tags, json_int(1));
json_set(*doc, "tags", tags);

text := to_json(doc);
assert(text == "{\"name\":\"Ada\",\"tags\":[1]}");
free(text);
free_json(*doc);
↗
:: struct

Where an encoder writes: the builder, whether to indent, and the nesting depth. Set builder (and pretty) and pass it to json_encode.

builder: *String_Builder
pretty: bool
depth: int
:: struct

Reads one JSON document. Every reading procedure returns false after recording an error in error; the first error wins, and callers must stop at once and return false themselves.

text: string
pos: int
depth: int
max_depth: int
error: Json_Error
key_start: int
where the key last returned by json_object_next begins
path: [..] Path_Segment
scratch: [..] u8
values: [..] Json_Value
children of the arrays being read into a Json_Value, innermost last
members: [..] Json_Member
the same for objects
index: [] u32
stage 1's structural index, when indexed (see index.jai)
ip: int
the first index entry that may lie at or after pos
indexed: bool
deny_unknown: bool
the options the generated decoders test
missing_default: bool
duplicate_last: bool
borrow: bool
:: struct

One key=value of a query or a form, already percent-decoded.

key: string
value: string

Constants#

:: u64.[ 0xeef453d6923bd65a, 0x113faa2906a13b3f, // 5^-342 0x9558b4661b6565f8, 0x4ac7ca59a424c507, // 5^-341 0xbaaee17fa23ebf76, 0x5d79bcf00d2df649, // 5^-340 0xe9 ...
:: u64.[ 0x4f0cedc95a718dd4, 0x5b01e8b09aa0d1b5, // k = -324 0x7e7b160ef71c1621, 0x119ca780f767b5ee, // k = -323 0x652f44d8c5b011b4, 0x0e16ec672c52f7f2, // k = -32 ...