From db304b2f8ec5058b30574cdc43ccc1f251e3a1bd Mon Sep 17 00:00:00 2001 From: Arham Wani Date: Fri, 31 Jul 2026 00:29:38 +0530 Subject: [PATCH] fix(shared): stop the lenient JSON parser from deleting commas inside strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `parseLenientJsonGetter` strips comments with string-aware regexes (an alternation that leaves quoted string literals untouched), but the trailing- comma pass right after it was not string-aware: stripped = stripped.replace(/,(\s*[}\]])/g, "$1"); Any string value containing a comma immediately before `}` or `]` had that comma silently removed. `{"note":"a,]"}` decoded to `{ note: "a]" }` and `{"list":["x,}"]}` to `{ list: ["x}"] }` — still valid JSON, so it parsed to a wrong value with no error. This runs on real config (`t3.json` via `fromLenientJson` / `T3ProjectFileFromJson`, and `ServerSettingsJson`). Make the pass string-aware in the same way as the two comment passes above it, so genuine trailing commas are still collapsed while commas inside string values are preserved. Added a test covering both. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/shared/src/schemaJson.test.ts | 9 +++++++++ packages/shared/src/schemaJson.ts | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/schemaJson.test.ts b/packages/shared/src/schemaJson.test.ts index c808a9b7c51..4a4d16da0b3 100644 --- a/packages/shared/src/schemaJson.test.ts +++ b/packages/shared/src/schemaJson.test.ts @@ -57,6 +57,15 @@ Done.`), expect(() => decodeLenientJson('{ "enabled": true,, }')).toThrow(); }); + it("preserves commas before brackets inside string values", () => { + // A comma inside a string value that happens to precede `}`/`]` must not + // be stripped as if it were a trailing comma. + expect(decodeLenientJson('{"note":"a,]"}')).toEqual({ note: "a,]" }); + expect(decodeLenientJson('{"list":["x,}"]}')).toEqual({ list: ["x,}"] }); + // Genuine trailing commas are still removed. + expect(decodeLenientJson('{"values":[1, 2,],}')).toEqual({ values: [1, 2] }); + }); + it("formats schema failures with paths without exposing invalid values", () => { const decodeCredential = decodeJsonResult(Schema.Struct({ token: Schema.Number })); const decoded = decodeCredential('{"token":"credential=secret-value"}'); diff --git a/packages/shared/src/schemaJson.ts b/packages/shared/src/schemaJson.ts index 04d26d9c229..77b1fa5d548 100644 --- a/packages/shared/src/schemaJson.ts +++ b/packages/shared/src/schemaJson.ts @@ -190,8 +190,14 @@ const parseLenientJsonGetter = SchemaGetter.onSome((input: string) => { (match, stringLiteral: string | undefined) => (stringLiteral ? match : ""), ); - // Strip trailing commas before `}` or `]`. - stripped = stripped.replace(/,(\s*[}\]])/g, "$1"); + // Strip trailing commas before `}` or `]`. The alternation preserves quoted + // strings so a comma inside a string value (e.g. `{"note":"a,]"}`) is not + // mistaken for a trailing comma and removed. + stripped = stripped.replace( + /("(?:[^"\\]|\\.)*")|,(\s*[}\]])/g, + (match, stringLiteral: string | undefined, bracket: string | undefined) => + stringLiteral ? match : (bracket ?? ""), + ); return decodeJsonString(stripped).pipe( Effect.map(Option.some),