Tutorials & Guides
How to Build a Robust XML-Based Prompting Pipeline for Claude 3.5 Sonnet to Guarantee Parsable JSON
Tired of Claude's conversational fluff breaking your JSON.parse() calls? Learn how to wrap outputs in strict XML tags and build a bulletproof parsing pipeline.
Updated 10/5/2026
The Flaw of Traditional JSON Mode
If you have tried to build automated workflows with Large Language Models, you have likely run into the dreaded JSON parsing error. You configure your prompt, specify a JSON schema, and call the API. Nine times out of ten, it works. But on the tenth run, the LLM decides to append a friendly introduction ("Here is the structured data you requested:"), wrap the JSON in markdown code blocks, or include trailing commas that turn your robust production server into a heap of unhandled exceptions.
While native tool-calling and strict JSON modes have improved, they still fail under pressure. This is especially true when dealing with highly complex schemas or asking models to write actual code blocks inside a JSON string.
However, Anthropic’s models are famously fine-tuned to respect XML. If you want Claude to perform flawlessly, the most reliable approach is to wrap your expected payload in clean, explicit XML tags, extract the raw block using regex, and only then parse it. This guarantees that your application behaves predictably, ticking every architectural box for a production-grade pipeline.
In this guide, we will set up an XML-encapsulated JSON parsing pipeline using Claude 3.5 Sonnet and Node.js/TypeScript.
Why Claude Loves XML Tags
During its pre-training and reinforcement learning phases, Anthropic structurally prompts Claude using XML tags. Because of this, Claude understands XML tags like <instruction>, <schema>, and <output> with surgical precision. It knows that anything inside <json_payload> is data, not conversation.
Using XML tags as boundaries gives us several major advantages:
1. Conversation Isolation: Claude can explain its reasoning outside the XML tags, while leaving the structured data clean and untainted inside them.
2. Resilience to Markdown Formatters: Even if Claude adds triple backticks (` `json `) inside the tag, we can easily strip them out with regex before parsing.
3. Easier Debugging: When reviewing logs, XML tags stand out clearly, making it instantly obvious if the model's output was cut short.
For more advanced structural patterns, you can use our built-in /prompts system to test and auto-compile templates containing custom XML schemas.
Step 1: Crafting the System Prompt
To make this work, we must explicitly instruct Claude to wrap its JSON schema inside custom XML tags. Let's create our prompt strategy. We will ask Claude to evaluate a code pull request and output its review in a structured format.
Here is how we set up the prompt template:
`typescript
const SYSTEM_PROMPT = `You are an elite code reviewer. Your task is to analyse the provided pull request patch and output a structured analysis.
You must place your final structured analysis inside a <review_payload> XML tag.
Here is the JSON schema you must adhere to inside the XML tags: { "status": "APPROVED" | "REQUEST_CHANGES", "criticalIssuesFound": boolean, "comments": [ { "line": number, "issue": string, "severity": "low" | "medium" | "high" } ] }
Strict Rules:
1. You are welcome to think out loud and write your thoughts, chain-of-thought analysis, and reasoning before you output the XML block.
2. However, the final JSON must be wrapped cleanly inside <review_payload> and </review_payload> tags.
3. Do not include markdown code block formatting (like \\\`json) inside the XML tags. Just output raw, valid JSON.
`;
`
By explicitly allowing Claude to write its thoughts outside the tags, we actually increase the accuracy of the JSON itself. Forcing an LLM to immediately start with a { restricts its ability to process complex logic sequentially before emitting its answer.
Step 2: Creating the Robust Parser
Next, we need a helper function that targets our specific XML tag, isolates the inner content, handles common LLM syntax oddities (like stray markdown formatting), and safely parses the clean JSON.
`typescript
export function extractAndParseJSON<T>(rawOutput: string, tagName: string): T {
// Regex matches everything between <tagName> and </tagName> across multiple lines
const regex = new RegExp(<${tagName}>([\\s\\S]*?)<\/${tagName}>);
const match = rawOutput.match(regex);
if (!match) {
throw new Error(Failed to locate XML tag <${tagName}> in model output.);
}
let jsonContent = match[1].trim();
// Edge case: Sometimes Claude still wraps the inner text in markdown code blocks despite instructions
if (jsonContent.startsWith("`")) {
// Remove leading `json or ` and trailing `
jsonContent = jsonContent.replace(/^`[a-zA-Z]*\n/, "").replace(/\n`$/, "").trim();
}
try {
return JSON.parse(jsonContent) as T;
} catch (error: any) {
throw new Error(Failed to parse extracted content to JSON: ${error.message}\nRaw Content: ${jsonContent});
}
}
`
Step 3: Stitching the Claude SDK Pipeline Together
Now, let's write our main execution script using the official Anthropic SDK. Ensure you have installed the SDK:
`bash
npm install @anthropic-ai/sdk
`
Create src/reviewer.ts:
`typescript
import Anthropic from '@anthropic-ai/sdk';
import { extractAndParseJSON } from './parser';
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, });
interface PullRequestReview { status: 'APPROVED' | 'REQUEST_CHANGES'; criticalIssuesFound: boolean; comments: Array<{ line: number; issue: string; severity: 'low' | 'medium' | 'high'; }>; }
async function runCodeReview(patchCode: string): Promise<PullRequestReview> { const userContent = `Review this code change:
${patchCode}`;
const response = await anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 1500, temperature: 0.1, // Lower temperature is vital for structured formatting tasks system: SYSTEM_PROMPT, messages: [ { role: 'user', content: userContent } ] });
const textOutput = response.content[0].type === 'text' ? response.content[0].text : ''; if (!textOutput) { throw new Error("Received empty response from Claude API"); }
// Safe, structured parsing via our XML wrapper
const reviewData = extractAndParseJSON<PullRequestReview>(textOutput, 'review_payload');
return reviewData;
}
`
Step 4: Gracefully Handling Edge Cases
What happens when a network blip or token truncation prevents Claude from outputting the closing </review_payload> tag? We can write a simple wrapper that attempts to repair common truncation errors, or gracefully routes back to the API for a quick self-correction loop.
If you want to read more about implementing automated retry loops for API errors, read our deep dive on troubleshooting Anthropic pipelines at /platforms/claude/articles.
Here is a simple auto-repair regex that closes the XML tag if it was cut off near the end:
`typescript
function repairTruncatedPayload(rawOutput: string, tagName: string): string {
const openTag = <${tagName}>;
const closeTag = </${tagName}>;
if (rawOutput.includes(openTag) && !rawOutput.includes(closeTag)) {
// Check if the output looks like it simply ran out of tokens before closing
return `${rawOutput}
</${tagName}>`;
}
return rawOutput;
}
`
By implementing this pattern, your dependency on third-party orchestration frameworks drops to zero. You do not need bloated library abstractions to parse output—just elegant, native XML boundaries designed to align with how Claude is natively trained. For live examples and more advanced prompt patterns, head over to Anthropic’s developer cookbook to see structured workflows in action.
Keep going
Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.