You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
constitems: any[]=[];for(leti=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}});constparsed=JSON.parse(JSON.stringify(items));constz=parsed[0].id;// <-- ONE read of ONE fieldconsole.log(JSON.stringify(parsed).length);
read parsed[i].id for all i (a sibling of the dropped field)
2234 ❌
3019
read parsed[0].nested.x — first 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 firstJSON.stringify(items) (a normal array) is correct — only stringifying a parsed value after a read is wrong.
Impact
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.
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.
JSON.stringifysilently drops a property from every element of an array returned byJSON.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
node/bun: 3019. Perry: 2234. Deterministic.The dropped field is
nested, on every record:What triggers it
parsed[i].nested.xfor all iparsed[i].idfor all i (a sibling of the dropped field)parsed[0].nested.x— first element onlyReading one property of one element corrupts the serialization of all 32. Which field you read does not matter.
What does NOT trigger it
nestedfirst / middle / last — all correct with a simplified record.{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).Likely area
benchmarks/json_polyglot/bench_field_access.ts's own header describes the mechanism this sits on: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
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.json_polyglot's publisher compares checksums across perry/node/bun and aborts withjson_polyglot/field_access: checksum mismatch ['2538318800', '2552985550'](perry2538318800, node and bun2552985550). The whole artifact therefore cannot be regenerated, which is what keepslintred.2026-07-13passed this same checksum gate, so this landed after that date.Also: the harness reports success when it fails
benchmarks/json_polyglot/run.shprintsand exits 0, while the embedded Python has already aborted via
raise SystemExit(...)and written nothing.run_public_baseline.shhasset -euo pipefailand 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.