Skip to content

std/json Spec

A JSON parser and serializer written in pure Raven over the __str_* byte intrinsics. parse is a recursive descent parser; stringify is a compact serializer. The value tree is the JsonValue enum.

Value type

enum JsonValue {
    Null,
    Bool(Bool),
    Number(Float),
    Str(String),
    Array(List<JsonValue>),
    Object(Map<String, JsonValue>),
}

Construct variants with the qualified form (JsonValue.Null, JsonValue.Bool(true), JsonValue.Number(1.0), JsonValue.Str(s), JsonValue.Array(list), JsonValue.Object(map)). Match with the bare variant names (Null, Bool(b), Number(n), ...).

Object uses std/collections Map<String, JsonValue>. The serializer reads map.keys() and map.values() to emit members; their order follows the Map hash-bucket layout, not insertion order.

Numbers are Float

Every JSON number, integer or not, parses to Float. Raven Float is an IEEE 754 double, so integers beyond the 53-bit mantissa (roughly 9.0e15) lose precision. There is no separate integer JSON type.

Import

import std/json { parse, stringify }

fun main() {
    match parse("{\"a\": 1, \"b\": [true, null]}") {
        Ok(v) -> print(stringify(v)),
        Err(e) -> print("bad json"),
    }
}

Surface

Item Result Notes
parse(text: String) Result<JsonValue, Error> full recursive descent parse
stringify(value: JsonValue) String compact (non-pretty) serialization
JsonValue.is_null(self) Bool true only for Null
JsonValue.as_bool(self) Option<Bool> Some only for Bool
JsonValue.as_number(self) Option<Float> Some only for Number
JsonValue.as_string(self) Option<String> Some only for Str
JsonValue.get(self, key: String) Option<JsonValue> object member, else None
JsonValue.at(self, i: Int) Option<JsonValue> array element, else None

The accessors return Option, so a wrong-kind or missing lookup is a normal None rather than an error.

Parsing

parse handles nested objects and arrays (including empty {} and []), strings, numbers, true, false, null, and inter-token whitespace (space, tab, newline, carriage return). It rejects any non-whitespace content after the top-level value.

String escapes

The two-character escapes \" \\ \/ \b \f \n \r \t decode to their usual bytes. A \uXXXX escape decodes a BMP code point and is encoded to UTF-8 bytes in the result.

Surrogate pairs

A high surrogate (\uD800 to \uDBFF) immediately followed by a low surrogate (\uDC00 to \uDFFF) decodes to the astral code point and is UTF-8 encoded. A high surrogate not followed by a low surrogate, or a lone low surrogate, decodes to U+FFFD (the replacement character). Note that the Raven lexer rejects surrogate \u escapes inside a source string literal, so a surrogate input reaches parse only from data read at runtime.

Numbers

A number is an optional minus, integer digits, an optional . fraction, and an optional e/E exponent with an optional sign. Digits accumulate into a Float (integer part built through the runtime's raven_int_to_float, fraction divided by a power of ten, exponent applied as a power-of-ten factor).

Errors

A parse failure is an std/error Error with kind "json". Raven has no type alias and a bundled module cannot call another bundled module's free functions, so the value is built directly as the Error struct literal (the same workaround std/fs and others use). The message names roughly what failed and, for unexpected or trailing bytes, the byte offset.

Serialization

stringify produces compact output with no spaces between tokens. Object members are emitted in the Map's key order. String escaping is the reverse of parsing: " and \ are escaped, control bytes below 0x20 use the \b \f \n \r \t shorthands where they exist and \u00XX otherwise, and every other byte (printable ASCII or a UTF-8 continuation byte) passes through unchanged.

A whole-number Float renders the way the runtime's Float-to-string produces it: 1.0 serializes as 1, and 0.15 serializes as 0.15.

Accessor implementation note

The payload-reading accessor methods (as_bool, as_number, as_string, is_null, get, at) forward to free functions that take the value as a plain parameter. Matching a self receiver and then reading the bound payload currently corrupts the extracted value in the back end, so every accessor that inspects a payload goes through a free helper. The serializer is already a free function for the same reason.

Serialization traits

std/json defines two traits for converting between a value and its JSON form, plus hand-written impls for the built-in types so field recursion bottoms out.

trait ToJson {
    fun to_json(self) -> JsonValue
}

trait FromJson {
    fun from_json(j: JsonValue) -> Result<Self, Error>
}

to_json is an ordinary self method. from_json is an associated function (no self), called as Point.from_json(j); the trait declares it with Self in the return, and an impl writes the concrete type, because the checker does not yet accept Self as a non-receiver type in a method signature.

Built-in impls: Int and Float map to a JSON number, Bool to a JSON bool, String to a JSON string, List<T> to a JSON array, and Option<T> to null or the inner value. An Int round-trips through Float (JSON has one number type) and loses precision beyond 2^53, and a number decodes back to Int by truncation toward zero.

User structs and enums get these traits through @derive(ToJson, FromJson). A struct serializes to an object keyed by field name; an enum to a tagged object {"tag": "Variant", "values": [...]}. See the derive spec for the full encoding and the helper functions the derive emits.

Item Result Notes
Int/Float/Bool/String.to_json(self) JsonValue scalar to its JSON kind
List<T: ToJson>.to_json(self) JsonValue JSON array
Option<T: ToJson>.to_json(self) JsonValue null or inner value
Type.from_json(j) Result<Type, Error> decode, Err on shape mismatch

The Float-to-Int truncation binds the raven_float_to_int runtime symbol through an extern "C" block, the counterpart to raven_int_to_float.

Out of scope

Pretty printing (indentation), duplicate-key policy beyond last-wins (the Map overwrites on a repeated key), and streaming or incremental parsing. These can be added later without changing the existing surface.