lib/json.bx

JSON, parsed and rendered, in ordinary Burxt.

use "lib/json.bx";

Nothing here needs a compiler feature. It is the enum + class mutual recursion from docs/guide/03-types.md and the Option/Result from lib/, which is the same test lib/map.bx passed: if a JSON document had needed a keyword, the type system would not be real.

—- The one position this library takes ————————————————

A JSON number is its DIGITS, not a float.

JSON’s own grammar says a number is arbitrary-precision decimal text. Every language then parses it into a double on the way in, which is where an exact 19.99 stops being exact — and it is the same abandonment as scaled exists to prevent at the C boundary. See spec/1.0/N1-BOUNDARY-EXACTNESS.md: real financial defects live at boundaries, not in arithmetic.

So Json.Number holds the digits exactly as they were written, and turning them into a typed value is a separate, checked step that can fail — json_as_int, json_as_money. Nothing is rounded on the way in, because nothing on the way in knows what rounding you wanted.

And going out, money crosses as a quoted string: json_money($19.99) renders "19.99", not 19.99. A JSON number reaches a JavaScript consumer as a double and loses the cent. A string reaches every consumer with all its digits. That costs one JSON.parse field being text on the far side, and it is the difference between exact and nearly.

What is in it

Name Kind What it answers
Json enum A JSON value.
Field class One member of an object. A class rather than a two-payload variant, because a name and a value travelling together is a
Reader class Where the parser is. A class with mutable self methods, because Burxt has no writable parameters — the same constraint
json_null function
json_truth function
json_int function
json_money function Money, as a quoted string. See the header: this is the position, and it is deliberate.
json_text function
json_list function
json_object function
json_field function
json_escape function A String with the six escapes JSON requires, and nothing else.
json_render function One JSON value as text, with no whitespace. Recursive, because the shape is.
json_at function A member by name, or None. Linear, like every other lookup in this repository at this scale — an MCP request has single-
json_as_text function
json_digits function The digits of a number, whether it arrived as a JSON number or as a quoted string.
json_as_int function
json_as_truth function
json_as_money function Money, at two places, or None when the digits are not exactly that.
is_json_digit function
json_parse function One JSON document. Trailing whitespace is allowed; trailing anything else is not, because a second document where one wa
skip_space method on Reader
peek method on Reader
parse_value method on Reader Parse one value at the cursor. Recursive through parse_list and parse_object.
word method on Reader Consume word if it is at the cursor. Answers whether it was.
parse_number method on Reader A number, kept as the digits it was written with. Only the SHAPE is checked here — that it is a number at all — because
parse_unicode_escape method on Reader One \uXXXX, with self.at sitting on the u. Answers the character and leaves self.at just past the escape. B9.
read_four_hex method on Reader The four hex digits of a \uXXXX, with self.at on the u. Leaves self.at past the digits.
parse_text method on Reader A quoted string, with the escapes undone.
parse_list method on Reader
parse_object method on Reader

Types

Json

enum Json

A JSON value.

List holds a slice of itself and Object a slice of Field, which holds a Json — the two halves of one recursive shape. A slice is a pointer and a length, so neither is infinitely wide, which is why this works where an enum directly inside an enum does not.

Source

Field

class Field { name: String, value: Json }

One member of an object. A class rather than a two-payload variant, because a name and a value travelling together is a thing worth naming — and because a variant may not carry an enum, while a class field may.

Source

Reader

class Reader { text: String, at: Int }

Where the parser is. A class with mutable self methods, because Burxt has no writable parameters — the same constraint that made lib/map.bx method-based, and the same outcome: the cursor being a value you can name reads better than threading an index through twelve returns.

Source

Functions

json_null

function json_null() -> Json

Source

json_truth

function json_truth(value: Bool) -> Json

Source

json_int

function json_int(value: Int) -> Json

Source

json_money

function json_money(amount: Decimal<2>) -> Json

Money, as a quoted string. See the header: this is the position, and it is deliberate.

Source

json_text

function json_text(value: String) -> Json

Source

json_list

function json_list(values: [Json]) -> Json

Source

json_object

function json_object(fields: [Field]) -> Json

Source

json_field

function json_field(name: String, value: Json) -> Field

Source

json_escape

function json_escape(text: String) -> String

A String with the six escapes JSON requires, and nothing else.

Built in RUNS rather than a byte at a time: out = out + one_byte copies the whole String on every byte, which this project has paid for three times — the lexer was quadratic for eleven versions on exactly this shape. Everything from copied to here is appended in one slice.

Source

json_render

function json_render(value: Json) -> String

One JSON value as text, with no whitespace. Recursive, because the shape is.

Source

json_at

function json_at(value: Json, name: String) -> Option<Json>

A member by name, or None. Linear, like every other lookup in this repository at this scale — an MCP request has single-digit field counts, and a map would allocate to save nothing.

Source

json_as_text

function json_as_text(value: Json) -> Option<String>

Source

json_digits

function json_digits(value: Json) -> Option<String>

The digits of a number, whether it arrived as a JSON number or as a quoted string.

Both, on purpose: an exact producer sends money as a string (see the header) and a careless one sends it as a number, and a server that reads only one of the two rejects half its callers for a difference that carries no information.

Source

json_as_int

function json_as_int(value: Json) -> Option<Int>

Source

json_as_truth

function json_as_truth(value: Json) -> Option<Bool>

Source

json_as_money

function json_as_money(value: Json) -> Option<Decimal<2>>

Money, at two places, or None when the digits are not exactly that.

It never rounds. "19.999" answers None rather than 20.00, because a value arriving from outside with more precision than you asked for is a question and not a rounding: the caller sent a third decimal place for a reason, and no default here can know what it was. 1.5 is fine — 1.50 loses nothing — and 1.567 is refused.

The reconstruction is a count of pennies times a penny, which is exact by construction and needs no rounding contract, because that is literally what a scaled decimal already is.

Source

is_json_digit

function is_json_digit(b: Int) -> Bool

Source

json_parse

function json_parse(text: String) -> Result<Json, String>

One JSON document. Trailing whitespace is allowed; trailing anything else is not, because a second document where one was expected means the caller framed its input wrong and saying so beats parsing half of it.

Source

Methods

skip_space

function (mutable self: Reader) skip_space() -> Int

Takes mutable self, so it changes the value it is called on.

Source

peek

function (self: Reader) peek() -> Int

Source

parse_value

function (mutable self: Reader) parse_value() -> Result<Json, String>

Parse one value at the cursor. Recursive through parse_list and parse_object.

Takes mutable self, so it changes the value it is called on.

Source

word

function (mutable self: Reader) word(word: String) -> Bool

Consume word if it is at the cursor. Answers whether it was.

Takes mutable self, so it changes the value it is called on.

Source

parse_number

function (mutable self: Reader) parse_number() -> Result<Json, String>

A number, kept as the digits it was written with. Only the SHAPE is checked here — that it is a number at all — because deciding what type it should become is the caller’s, and doing it here would be the silent conversion this file exists to avoid.

Takes mutable self, so it changes the value it is called on.

Source

parse_unicode_escape

function (mutable self: Reader) parse_unicode_escape() -> Result<String, String>

One \uXXXX, with self.at sitting on the u. Answers the character and leaves self.at just past the escape. B9.

Surrogate pairs are the reason this is a function rather than four lines inline. JSON is specified in terms of UTF-16, so every codepoint above U+FFFF is written as TWO escapes — an emoji is 😀, never one escape. A decoder that treats each \uXXXX independently produces two half-characters, and from_codepoint refuses those outright (they are surrogates, and encoding one is CESU-8, which this library’s own is_valid_utf8 rejects). So the choice was never “handle pairs or ignore them” — it was “handle pairs or refuse every emoji in real-world JSON”.

A lone surrogate, high or low, is an error rather than U+FFFD. Substituting a replacement character is the same silent repair as the "?" that os_byte_as_string used to make: the caller asked for text and would get text, subtly not the text that was sent.

Takes mutable self, so it changes the value it is called on.

Source

read_four_hex

function (mutable self: Reader) read_four_hex() -> Result<Int, String>

The four hex digits of a \uXXXX, with self.at on the u. Leaves self.at past the digits.

Takes mutable self, so it changes the value it is called on.

Source

parse_text

function (mutable self: Reader) parse_text() -> Result<String, String>

A quoted string, with the escapes undone.

Takes mutable self, so it changes the value it is called on.

Source

parse_list

function (mutable self: Reader) parse_list() -> Result<Json, String>

Takes mutable self, so it changes the value it is called on.

Source

parse_object

function (mutable self: Reader) parse_object() -> Result<Json, String>

Takes mutable self, so it changes the value it is called on.

Source