JsonWriter
struct JsonWriterAppend-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: Intindent: Stringpretty: Bool: Whetherindentis 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.