Skip to content

JSON.stringify(parseResult) returns "null" for objects with >=9 fields #307

Description

@proggeramlug

Summary

JSON.stringify(obj) returns the literal string "null" when obj is the result of JSON.parse(...) and has 9 or more fields. Object-literal counterparts with the same shape stringify correctly. Field access on the parsed object works (the data is there) — only re-serialization is broken.

This silently broke perry-hub: every POST /api/v1/build request had its manifest parsed correctly, but when the hub re-stringified job.manifest to send job_assign over WS to a worker, the manifest field became null. Workers received {"type":"job_assign","manifest":null,...} and rejected with "missing field app_name" — every CI build failed sub-second with no error in the user-visible output.

Repro (minimal)

repro.ts:

// Object literal — works at any size
const lit9 = {a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9};
console.log("literal-9: " + JSON.stringify(lit9));
//   literal-9: {"a":1,"b":2,"c":3,"d":4,"e":5,"f":6,"g":7,"h":8,"i":9}

// JSON.parse output — broken at >=9 fields
const src = '{"a":1,"b":2,"c":3,"d":4,"e":5,"f":6,"g":7,"h":8,"i":9}';
const round = JSON.parse(src);
console.log("typeof parsed=" + typeof round);   // object
console.log("round.a=" + round.a + " round.i=" + round.i);  // 1, 9
console.log("re-stringify: " + JSON.stringify(round));      // null  ← BUG
perry compile repro.ts -o repro && ./repro

Output:

literal-9: {"a":1,"b":2,"c":3,"d":4,"e":5,"f":6,"g":7,"h":8,"i":9}
typeof parsed=object
round.a=1 round.i=9
re-stringify: null

Threshold

Iterated n = 1..20 constructing dynamic objects via obj["field_" + i] = i:

n result
1–8 full JSON, length scales linearly
9 "null" (length 4)
10–20 "null"

The boundary is exactly at n = 9 (the same as JSON.parse-generated objects). Tightly suggests an internal [T; 8] or capacity-8 buffer in the runtime's JSON.stringify object emit path that overflows silently and falls through to a null fallback.

Object literals construct via a different path (the codegen emits direct field stores into a literal-object header) and aren't affected — only objects constructed dynamically (via obj[k] = v or JSON.parse) trip the bug.

Suspected location

crates/perry-runtime/src/json.rsstringify_object_inner and the shape-template fast path it dispatches to (shape_template_for / try_emit_shape_element / build_shape_prefix_template). The fast path activates at num_fields >= 5, but the broken threshold is 9 — so it's likely a secondary structure (perhaps keys_array capacity, or one of the inline buffers in ObjectHeader/ArrayHeader at runtime) sized 8 that gets exceeded for parser-built objects but not literal-built ones. Worth grepping that file for 8/MAX_INLINE_KEYS/similar caps.

Impact

  • Any program that round-trips JSON (parse, mutate or pass through, stringify) silently corrupts payloads of 9+ fields.
  • Perry-hub: every CI build job rejected with no actionable error. Rolled back to a hub binary built with perry 0.5.162 to recover.
  • Likely affects any HTTP server forwarding JSON, any database row mapper, anything that parses then re-emits.

Environment

  • perry version: 0.5.390 (also reproduces at 0.5.349, 0.5.388 — appears to have been introduced sometime between 0.5.162 and 0.5.349; an older perry 0.5.162 binary stringifies the same input correctly)
  • Platform-independent — pure runtime/codegen issue.

Suggested fix beyond the immediate cap-bump

Once the underlying buffer is enlarged or generalized: make the JSON.stringify fall-through return the actual error (or panic in debug) rather than the string "null". The current behavior is the worst possible failure mode — same exit path as legitimately stringifying null, which is why the hub silently corrupted job dispatch for two days before anyone could pin it down. A unique sentinel or a thrown TypeError would have surfaced this on the first job.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions