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),