Skip to content

Commit e44dc1b

Browse files
authored
fix(core): handle backticks in structured output (#8954)
1 parent e0b48fd commit e44dc1b

File tree

3 files changed

+67
-3
lines changed

3 files changed

+67
-3
lines changed

.changeset/thick-lamps-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/core": patch
3+
---
4+
5+
handle backticks in structured output

langchain-core/src/output_parsers/structured.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,15 @@ ${JSON.stringify(toJsonSchema(this.schema))}
106106
*/
107107
async parse(text: string): Promise<InferInteropZodOutput<T>> {
108108
try {
109-
const json = text.includes("```")
110-
? text.trim().split(/```(?:json)?/)[1]
111-
: text.trim();
109+
const trimmedText = text.trim();
110+
111+
const json =
112+
// first case: if back ticks appear at the start of the text
113+
trimmedText.match(/^```(?:json)?\s*([\s\S]*?)```/)?.[1] ||
114+
// second case: if back ticks with `json` appear anywhere in the text
115+
trimmedText.match(/```json\s*([\s\S]*?)```/)?.[1] ||
116+
// otherwise, return the trimmed text
117+
trimmedText;
112118

113119
const escapedJson = json
114120
.replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, (_match, capturedGroup) => {

langchain-core/src/output_parsers/tests/structured.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,56 @@ describe("StructuredOutputParser.fromZodSchema parsing newlines", () => {
381381
);
382382
});
383383
});
384+
385+
// https://github.com/langchain-ai/langchainjs/issues/8339
386+
describe("StructuredOutputParser.fromZodSchema parsing json with backticks", () => {
387+
const zod3Schema = z.object({
388+
name: z.string().describe("Name"),
389+
biograph: z.string().describe("Biograph in markdown"),
390+
});
391+
392+
const zod4Schema = z4.object({
393+
name: z4.string().describe("Name"),
394+
biograph: z4.string().describe("Biograph in markdown"),
395+
});
396+
397+
describe("without backticks", () => {
398+
const output =
399+
'{"name": "John Doe", "biograph": "john doe is a cool dude"}';
400+
const testCase = (schema: InteropZodType) => async () => {
401+
const parser = StructuredOutputParser.fromZodSchema(schema);
402+
const result = await parser.parse(output);
403+
expect(result).toHaveProperty("name", "John Doe");
404+
expect(result).toHaveProperty("biograph", "john doe is a cool dude");
405+
};
406+
test("zod v3", testCase(zod3Schema));
407+
test("zod v4", testCase(zod4Schema));
408+
});
409+
describe("with outer backticks", () => {
410+
const output =
411+
'```json\n{"name": "John Doe", "biograph": "john doe is a cool dude"}```';
412+
const testCase = (schema: InteropZodType) => async () => {
413+
const parser = StructuredOutputParser.fromZodSchema(schema);
414+
const result = await parser.parse(output);
415+
expect(result).toHaveProperty("name", "John Doe");
416+
expect(result).toHaveProperty("biograph", "john doe is a cool dude");
417+
};
418+
test("zod v3", testCase(zod3Schema));
419+
test("zod v4", testCase(zod4Schema));
420+
});
421+
describe("with inner backticks", () => {
422+
const output =
423+
'{"name": "John Doe", "biograph": "john doe is a ```cool dude```"}';
424+
const testCase = (schema: InteropZodType) => async () => {
425+
const parser = StructuredOutputParser.fromZodSchema(schema);
426+
const result = await parser.parse(output);
427+
expect(result).toHaveProperty("name", "John Doe");
428+
expect(result).toHaveProperty(
429+
"biograph",
430+
"john doe is a ```cool dude```"
431+
);
432+
};
433+
test("zod v3", testCase(zod3Schema));
434+
test("zod v4", testCase(zod4Schema));
435+
});
436+
});

0 commit comments

Comments
 (0)