authorgravatar for 132888838+arhxam@users.noreply.github.comArham Amin <132888838+arhxam@users.noreply.github.com> 2026-07-31 03:46:22+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-30 22:16:22+00:00
log32407df9c5ffd3c5dda2e745568997ac33c15ed1
treedaf2abf5312b3c8ba744ed5f97e0ac5cd08dd7e1
parent162e774294f9e3dda15ca2f8083db019fe398837
signature Signed by PGP key B5690EEEBB952194

fix(shared): lenient JSON parser deletes commas inside string values (#5025)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

2 files changed, 17 insertions(+), 2 deletions(-)

packages/shared/src/schemaJson.test.ts+9
...@@ -57,6 +57,15 @@ Done.`),...@@ -57,6 +57,15 @@ Done.`),
57 expect(() => decodeLenientJson('{ "enabled": true,, }')).toThrow();57 expect(() => decodeLenientJson('{ "enabled": true,, }')).toThrow();
58 });58 });
5959
60 it("preserves commas before brackets inside string values", () => {
61 // A comma inside a string value that happens to precede `}`/`]` must not
62 // be stripped as if it were a trailing comma.
63 expect(decodeLenientJson('{"note":"a,]"}')).toEqual({ note: "a,]" });
64 expect(decodeLenientJson('{"list":["x,}"]}')).toEqual({ list: ["x,}"] });
65 // Genuine trailing commas are still removed.
66 expect(decodeLenientJson('{"values":[1, 2,],}')).toEqual({ values: [1, 2] });
67 });
68
60 it("formats schema failures with paths without exposing invalid values", () => {69 it("formats schema failures with paths without exposing invalid values", () => {
61 const decodeCredential = decodeJsonResult(Schema.Struct({ token: Schema.Number }));70 const decodeCredential = decodeJsonResult(Schema.Struct({ token: Schema.Number }));
62 const decoded = decodeCredential('{"token":"credential=secret-value"}');71 const decoded = decodeCredential('{"token":"credential=secret-value"}');
packages/shared/src/schemaJson.ts+8-2
...@@ -190,8 +190,14 @@ const parseLenientJsonGetter = SchemaGetter.onSome((input: string) => {...@@ -190,8 +190,14 @@ const parseLenientJsonGetter = SchemaGetter.onSome((input: string) => {
190 (match, stringLiteral: string | undefined) => (stringLiteral ? match : ""),190 (match, stringLiteral: string | undefined) => (stringLiteral ? match : ""),
191 );191 );
192192
193 // Strip trailing commas before `}` or `]`.193 // Strip trailing commas before `}` or `]`. The alternation preserves quoted
194 stripped = stripped.replace(/,(\s*[}\]])/g, "$1");194 // strings so a comma inside a string value (e.g. `{"note":"a,]"}`) is not
195 // mistaken for a trailing comma and removed.
196 stripped = stripped.replace(
197 /("(?:[^"\\]|\\.)*")|,(\s*[}\]])/g,
198 (match, stringLiteral: string | undefined, bracket: string | undefined) =>
199 stringLiteral ? match : (bracket ?? ""),
200 );
195201
196 return decodeJsonString(stripped).pipe(202 return decodeJsonString(stripped).pipe(
197 Effect.map(Option.some),203 Effect.map(Option.some),