Skip to content

JSON.stringify drops a field from every element of a JSON.parse'd array once any property is read (silent data loss; blocks #7257) #7264

Description

@proggeramlug

JSON.stringify silently drops a property from every element of an array returned by JSON.parse, as soon as any property of any element is read. No error, no warning — the output is just short.

Found by the public-baseline gate, which correctly refused to publish (see "Impact").

Reproducer

const items: any[] = [];
for (let i = 0; i < 32; i++) items.push({
  id: i, name: "item_" + i, value: i * 3.14159,
  tags: ["tag_" + (i % 10), "tag_" + (i % 5)], nested: { x: i, y: i * 2 }
});
const parsed = JSON.parse(JSON.stringify(items));
const z = parsed[0].id;                    // <-- ONE read of ONE field
console.log(JSON.stringify(parsed).length);

node/bun: 3019. Perry: 2234. Deterministic.

The dropped field is nested, on every record:

node:  ..."tags":["tag_8","tag_3"],"nested":{"x":98,"y":196}}
perry: ..."tags":["tag_8","tag_3"]}

What triggers it

case perry node
parse, then stringify with no property read 3019 ✅ 3019
read parsed[i].nested.x for all i 2234 3019
read parsed[i].id for all i (a sibling of the dropped field) 2234 3019
read parsed[0].nested.xfirst element only 2234 3019

Reading one property of one element corrupts the serialization of all 32. Which field you read does not matter.

What does NOT trigger it

  • No property read at all → correct.
  • Property position: nested first / middle / last — all correct with a simplified record.
  • Blob size alone: {id, a:"x", nested:{x:i}} is correct at N=256 / 9509 bytes, while the record above fails at N=16 / 1499 bytes. So this is shape-dependent, not size-dependent. I did not minimise further; the difference between the two shapes is a computed string ("item_"+i), a float (i*3.14159), and an array of computed strings (tags).
  • Scale: correct at N=10, broken from N=16 up. Loss is a stable ~26% of output length.

Likely area

benchmarks/json_polyglot/bench_field_access.ts's own header describes the mechanism this sits on:

"bench.ts is a parse+stringify roundtrip that Perry's lazy tape can memcpy without materializing, this one TOUCHES EVERY ELEMENT, which forces full materialization."

The symptom is exactly "materialisation triggered by a read loses a field", so the lazy-tape materialisation path is the first place to look. Note the first JSON.stringify(items) (a normal array) is correct — only stringifying a parsed value after a read is wrong.

Impact

  1. Silent data loss in a core API. JSON.stringify(JSON.parse(x)) is a routine idiom, and this loses a field with no diagnostic. Anything that parses JSON, reads a field, and re-serialises — a proxy, a cache layer, a request handler — can emit truncated records.
  2. Blocks lint: the public-baseline freshness gate has been failing for 40+ commits, making every later lint step unreachable #7257 / the public baseline. json_polyglot's publisher compares checksums across perry/node/bun and aborts with json_polyglot/field_access: checksum mismatch ['2538318800', '2552985550'] (perry 2538318800, node and bun 2552985550). The whole artifact therefore cannot be regenerated, which is what keeps lint red.
  3. It is a regression. The published baseline dated 2026-07-13 passed this same checksum gate, so this landed after that date.

Also: the harness reports success when it fails

benchmarks/json_polyglot/run.sh prints

Machine-readable results: .../json-polyglot.json

and exits 0, while the embedded Python has already aborted via raise SystemExit(...) and written nothing. run_public_baseline.sh has set -euo pipefail and still sailed past it, only failing ~40 minutes later at final assembly with a confusing "could not load json-polyglot.json". The success line should not print unless the file was written, and a checksum mismatch should fail the leg loudly and immediately. Worth splitting out if you'd prefer it tracked separately.

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