JSON Cursor Adapters v1

September 10, 2026 · View on GitHub

Status: implemented bounded adapters; HOSTED GREEN under the v0.4.0 release baseline, including the admitted standalone, Project v16 and cross-package roundtrip profiles.

Audience: language users, standard-library contributors, and backend implementers.

This specification adds small, allocation-free adapters between the existing std.io cursors and the existing bounded JSON packages. It extends the packages without replacing or changing any old API. A Reader and Writer remain the ordinary nongeneric records from IO Cursors v1: each owns a caller-supplied Bytes value and carries a usize cursor. These functions do not introduce streams, handles, files, or host authority.

API surface

The additions are source functions with these signatures:

std.data.json.dec.decode_into(
    input: borrow std.io.Reader,
    output: own std.io.Writer
) -> std.io.Writer

std.data.json.write.quoted_into(
    input: borrow std.io.Reader,
    output: own std.io.Writer
) -> std.io.Writer

std.data.json.write.count_into(
    value: usize,
    output: own std.io.Writer
) -> std.io.Writer

decode_into consumes the output writer and returns its advanced replacement. The input is borrowed and remains available, with its cursor and owned buffer unchanged. quoted_into has the same ownership and cursor behavior; count_into consumes a scalar count and the output Writer. As with writer_write_u8, a successful operation advances the output cursor once per emitted byte; writer_finish returns the original caller buffer, including its unwritten suffix.

These are additive package APIs. Existing scanner, decoder, quoted-byte, decimal-byte, Reader, and Writer functions retain their signatures, IDs, and meaning. The adapters do not add compiler operations, a hidden allocation, a public nominal ABI, or an owned JSON tree.

Reader position and preflight

The string adapters read bytes beginning at the borrowed reader's current position. The position must be within the reader buffer, as required by the existing cursor observers. A borrowed input is never advanced: callers that need a new cursor can use the existing owned reader_advance transition after the operation.

Every byte count and every input span is computed before the first output byte is written. The output writer's position must likewise be within its buffer, and its remaining capacity must be at least the complete result size. A capacity failure occurs before any buffer write or cursor update; there is no partial JSON output. The consumed writer follows ordinary failure cleanup and is not returned to the caller. The same all-or-nothing preflight applies to malformed or incomplete input. The ordinary checked contract and failure selection rules decide how a rejected call is reported; this specification does not introduce a new result encoding.

The preflight is exact and uses the caller's actual buffer length. The old std.data.json.dec.capacity() value of 256 remains an API for the bounded comparison helper; it is not a limit on these cursor adapters. Caller output buffers larger than 256 bytes are valid, and an adapter may fill any capacity that its exact preflight proves sufficient.

decode_into

std.data.json.dec.decode_into decodes one complete JSON string token at the reader cursor. It applies the existing std.data.json.dec rules: the opening and closing quotes are required, simple escapes and \uXXXX escapes are expanded, strict surrogate pairing is enforced, raw control bytes are rejected, and raw bytes that are admitted by the existing decoder pass through unchanged. The token's decoded length is determined with decoded_len before the writer is touched, then the exact decoded bytes are emitted in source order.

The token must be complete in the reader's remaining range. Bytes after its closing quote remain unread and are not consumed or interpreted by this operation; callers that require a whole input can first apply the existing full-string scanner policy. A string that is truncated, malformed, or whose decoded result does not fit the writer is rejected during preflight, before any output mutation. An empty JSON string is valid and leaves the output cursor unchanged after the successful zero-byte emission.

The operation has the same bounded meaning as decoded_size and the same UTF-8 policy: it expands escapes to UTF-8 for escaped scalar values, while preserving admitted raw input bytes. It does not validate a raw UTF-8 sequence that the existing decoder does not validate. It does not retain a decoded string object, and no decoded view escapes the call.

decoded_token_eq

std.data.json.dec.decoded_token_eq(input, left, right) compares two decoded JSON string tokens in input for equality. It walks both tokens through the package's existing pull surface (emit_at, emit_len, token_end), the same surface decode_into and the caller-streamed token loop use, so escapes decode to their scalar value before either side is compared: a simple escape and a \uXXXX escape for the same character compare equal, and "ab" equals a separately positioned "ab". Comparison rejects up front when either token's decoded_len is a failure or when the two tokens' decoded lengths differ, and otherwise compares decoded bytes one emitted position at a time. No intermediate buffer is allocated; unlike decoded_eq, this comparison is not bound by the package's 256-byte comparison capacity.

This is a token-level comparison only. Duplicate-key detection over a JSON document — walking an object's members and reporting a repeated name — is still Missing here; that object walk lives in std.data.json.doc, which compares member name spans byte for byte and does not yet consult decoded values either.

quoted_into

std.data.json.write.quoted_into emits a quoted JSON string for every byte in the reader's remaining range. Its size is the exact sum of the opening and closing quotes plus quoted_len's per-byte widths. Its bytes are exactly the ones defined by quoted_byte and escape_byte:

  • " and \\ use their two-byte named escapes;
  • backspace, tab, line feed, form feed, and carriage return use the existing short escapes;
  • other bytes below 0x20 use lowercase \u00hh escapes;
  • all other bytes pass through unchanged.

The full quoted size is preflighted before writing the opening quote, so a short writer cannot receive a prefix. The input remains borrowed and its remaining bytes are copied in order into the JSON representation. Bytes after the reader cursor are all included; there is no implicit terminator or extra whitespace.

count_into

std.data.json.write.count_into writes the decimal ASCII representation of its value: usize argument into the owned writer and returns the advanced writer. It uses the existing usize_len and usize_byte policy, with no sign and no leading zeroes except for the value zero. The decimal width is preflighted before the first digit is written, and the writer advances by exactly the number of decimal digits. The function does not read or modify a Reader.

Evaluation, ownership, and backends

Arguments and all helper calls evaluate left to right. Input validation, decoded or encoded length calculation, and output-capacity validation complete before output mutation. A successful call transfers the output Writer as one ordinary owned value and publishes its advanced cursor. A rejected call does not publish a partially written writer or fabricated input state. Existing cleanup and sticky failure selection remain authoritative.

The adapters are source-level compositions of existing cursor, JSON scanner, decoder, and writer operations. They require no new compiler primitive and grant no ambient filesystem, process, network, home, secret, key, wallet, or signing capability. Interpreter, native C11, and Core Wasm implementations must consume the same checked HIR meaning. No backend may infer validity from the Reader or Writer carrier layout.

This document makes no public hosted-I/O, physical-device, production, nominal ABI, growable-buffer, or owned-document-tree claim. The implemented adapter corpus has hosted-green release evidence; the broader Everyday profile remains partial.

Project composition

The decoder retains owned-data-api.v1. The writer uses the additive useful-data.v2 (Project v16) profile: private cursor calls use checked owned data, while its existing quoted_len web export retains the frozen Useful Data v1 public projection. A public owning or nominal cursor export is not admitted. Both packages declare the exact bundled std.io dependency.

quoted_remaining_len observes the encoded size of the Reader remainder without consuming either buffer. Independent cursor conformance programs use a two-entry byte arena; legacy decoder conformance keeps its one-entry arena. The new cases include a 300-byte decoded string and sentinel bytes outside the output prefix. These tests do not add an owned document tree or a streaming parser.

Local verification record

The original local standalone decoder and writer cursor corpus passed on the interpreter, native C11 at -O0 and -O2, and Core Wasm. It included a 300-byte decoded string, demonstrating that the adapters are not limited by the decoder's old 256-byte comparison buffer. Six malformed-input, insufficient-capacity, and forged-cursor contract-rejection cases also passed with no partial output or cursor publication.

Project v16's profile_admission::project_v16_json_cursor_public_facade_replays_and_executes gate covers deterministic npm reconstruction, envelope replay, and repeated Node execution. Cross-package decode/requote roundtrip uses the private_json_cursor_roundtrip_executes_across_project_backends gate on the interpreter entry and repeated test, native C11 at -O0 and -O2, and repeated Core Wasm with a strict two-entry arena. The unchanged 16 MiB workspace budget fits this gate. The historical local observations retain their original scope; the implemented release corpus is hosted green without changing the public export or nominal-ABI boundary.