Reliable Structured Output for Web Features
When your UI needs JSON, don't parse it out of prose — use tools or a JSON schema so every response is shaped exactly the way your code expects.
The moment Claude's output feeds a database write, a form, or a chart, you need structured data, not a paragraph you regex your way through. Two supported approaches make this reliable.
Tools: when Claude should trigger an action
Define a tool with a JSON Schema for its inputs. Claude responds with a tool_use block whose input already matches your schema. This is ideal when the model is deciding whether and how to do something — look up an order, create a calendar event, file a ticket. Add strict: true to the tool definition (with additionalProperties: false and a required list) to guarantee the arguments validate exactly.
JSON output: when you just want data back
When you are not calling a function but simply want the answer as data — extract these fields, classify into these buckets — set output_config.format to a JSON schema. The response text is then guaranteed valid JSON in your shape.
Parse defensively anyway
Even with these guarantees, check stop_reason. If it is max_tokens the JSON was cut off mid-object; if it is refusal you will not get your shape at all. Handle both before you JSON.parse.
Example
// Feature: turn a support message into a structured ticket for your DB.
const res = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 512,
messages: [{ role: "user", content: supportMessage }],
output_config: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
category: { type: "string", enum: ["billing", "bug", "feature", "other"] },
priority: { type: "string", enum: ["low", "medium", "high"] },
summary: { type: "string" },
},
required: ["category", "priority", "summary"],
additionalProperties: false,
},
},
},
});
// stop_reason guard BEFORE parsing — truncated JSON is invalid JSON.
if (res.stop_reason !== "end_turn") throw new Error(`unexpected stop: ${res.stop_reason}`);
const text = res.content.find((b) => b.type === "text").text;
const ticket = JSON.parse(text); // { category, priority, summary }When to use it
- Use a tool definition to extract invoice line items into a typed array that feeds directly into a database insert.
- Force JSON output for a product-tagging feature so the tags array is always parseable without defensive string splitting.
- Return a structured { title, summary, tags } object from Claude for each blog post to populate CMS fields automatically.
More examples
Force structured output with tool_choice
Setting tool_choice forces Claude to always call save_lead, returning a validated JS object every time.
const msg = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 256,
tools: [{
name: "save_lead",
description: "Save an extracted sales lead.",
input_schema: {
type: "object",
properties: {
name: { type: "string" },
company: { type: "string" },
email: { type: "string", format: "email" },
budget: { type: "number", description: "Estimated budget in USD" }
},
required: ["name", "email"]
}
}],
tool_choice: { type: "tool", name: "save_lead" },
messages: [{ role: "user", content: emailText }],
});
const lead = msg.content[0].input; // typed JS object, no JSON.parseJSON schema in the system prompt
Embeds the JSON Schema in the system prompt so Claude self-validates its output against the required shape.
const schema = JSON.stringify({
type: "object",
properties: {
sentiment: { type: "string", enum: ["positive","negative","neutral"] },
score: { type: "number", minimum: 0, maximum: 10 },
summary: { type: "string", maxLength: 100 }
},
required: ["sentiment", "score", "summary"]
});
const msg = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 128,
system: `Respond ONLY with valid JSON matching this schema:\n${schema}`,
messages: [{ role: "user", content: review }],
});
const result = JSON.parse(msg.content[0].text);Defensive parse with schema validation
Strips code fences, parses JSON, and runs Ajv schema validation to catch any shape deviations before using the data.
import Ajv from "ajv";
const ajv = new Ajv();
const validate = ajv.compile(mySchema);
function parseAndValidate(text) {
const clean = text.replace(/^```json\n?/, "").replace(/```$/, "").trim();
const data = JSON.parse(clean);
if (!validate(data)) throw new Error("Schema mismatch: " + ajv.errorsText(validate.errors));
return data;
}
Discussion