json_mlake ยท API

json.writer

Structs

JsonWriter

struct JsonWriter

Append-only JSON byte sink over a pre-sized buffer.

Construct with a capacity estimate, write, then finish(). The estimate does not have to be right -- ensure grows the buffer when it is short -- but a good estimate means the hot path never checks anything but the cursor.

Example: var w = JsonWriter(capacity=64) w.begin_object() w.key("name") w.write_string("Ada") w.end_object() var out = w^.finish_string()

Implements Movable

Fields
buf: List[UInt8]
pos: Int
indent: String
pretty: Bool: Whether indent is non-empty, hoisted so the compact path is one predictable branch per child rather than a string length check.
depth: Int

__init__

def __init__(out self, *, capacity: Int = Int(256))
def __init__(out self, *, capacity: Int = Int(256), indent: String)

Pretty-printing writer. indent is one level of indentation.

def __init__(out self, var buf: List[UInt8])

Adopt a caller-owned buffer, so allocations can be reused.

ensure

def ensure(mut self, n: Int)

Make room for n more bytes. Cold path when sized correctly.

write_byte

def write_byte(mut self, c: UInt8)

write_bytes

def write_bytes(mut self, data: Span[UInt8])

write_literal

def write_literal(mut self, s: StringSpan[ImmStaticOrigin])

reset

def reset(mut self)

Rewind to empty, keeping the buffer.

For a caller serializing many values in a row: the allocation is the expensive part and it is already the right size.

write_null

def write_null(mut self)

write_bool

def write_bool(mut self, b: Bool)

write_int

def write_int(mut self, v: Int64)

Decimal integer, written backwards two digits at a time.

The magnitude is taken in unsigned arithmetic, so Int64.MIN -- which has no positive counterpart -- needs no special case: negating it as UInt64 gives exactly 2**63.

write_uint

def write_uint(mut self, v: UInt64)

Decimal integer above the signed range.

JSON puts no upper bound on an integer, so a document may hold a value that only fits unsigned. Writing it through the signed path would wrap it into a negative number, silently.

write_float

def write_float(mut self, v: Float64)

A float as the shortest decimal that reads back as itself.

Digits come from json.dtoa, not from String(Float64): the stdlib formatter does not always produce a representation that round-trips, and a serializer that writes a number nothing can read back as the value given to it has lost the one property it exists to keep.

Layout follows the convention this library already emitted -- fixed notation while the leading digit sits between 1e-5 and 1e15, scientific outside that, exponent always signed and at least two digits. Integer-valued floats keep a trailing .0 so that a value written as a float reads back as one.

A non-finite value writes null. JSON has no spelling for infinity or NaN, so the alternatives are an invalid document or a refusal; refusing needs a policy the caller sets, which SerializerConfig grows separately.

write_string

def write_string[ascii_only: Bool = False, solidus: Bool = False](mut self, s: String)

A quoted, escaped JSON string literal.

Both default off and are parameters rather than fields so the ordinary path compiles to exactly what it did before they existed. Carrying them as Bool fields instead measured about eight percent on document@100 serialize even with every read of them removed.

Parameters
ascii_only: Escape every code point above U+007F as \uXXXX. A character outside the basic plane becomes the surrogate pair JSON spells it with.
solidus: Escape / as \/. JSON does not require it, but embedding output in a <script> element does.

write_string_span

def write_string_span[ascii_only: Bool = False, solidus: Bool = False](mut self, b: Span[UInt8])

A quoted, escaped JSON string literal.

One pass, not two. The scan that decides whether anything needs escaping used to be separate from the copy, so every string was read twice even though the scan already knows where the clean run ends. Here the first chunk with something to escape hands straight over to the escape path, which restarts from the beginning; a string with no escapes -- the usual case -- is a quote, one memcpy and a quote.

begin_object

def begin_object(mut self)

end_object

def end_object(mut self)

begin_array

def begin_array(mut self)

end_array

def end_array(mut self)

key

def key(mut self, k: String)

An object key plus its colon.

value_written

def value_written(mut self)

Mark that a value completed, so the next member emits a comma.

item_null

def item_null(mut self)

item_bool

def item_bool(mut self, b: Bool)

item_int

def item_int(mut self, v: Int64)

item_float

def item_float(mut self, v: Float64)

item_string

def item_string(mut self, s: String)

open_container

def open_container(mut self, brace: UInt8)

close_container

def close_container(mut self, brace: UInt8, empty: Bool)

next_child

def next_child(mut self, first: Bool)

Separator before a container child.

The compact case -- every child of every container, in the overwhelmingly common configuration -- is one comma and one correctly predicted branch. It used to call into the newline helper regardless, which read the indent string's length per child before deciding to do nothing.

colon

def colon(mut self)

finish

def finish(deinit self) -> List[UInt8]

The written bytes. Moves the buffer out; no copy.

finish_string

def finish_string(deinit self) -> String

The written bytes as a String.

Functions

needs_escape

def needs_escape(b: Span[UInt8]) -> Bool

True if any byte requires escaping in a JSON string literal.

One SIMD pass testing == '"', == '\' and < 0x20 per byte. The answer is almost always False, which is what makes the memcpy fast path in write_string worth having.