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

Example · javascript
// 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.

Example · javascript
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.parse

JSON schema in the system prompt

Embeds the JSON Schema in the system prompt so Claude self-validates its output against the required shape.

Example · javascript
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.

Example · javascript
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

  • Be the first to comment on this lesson.