cd /news/large-language-models/structured-outputs-vs-tool-use-vs-pr… · home topics large-language-models article
[ARTICLE · art-51355] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=· neutral

Structured Outputs vs Tool Use vs Prefills: Getting JSON Out of Claude in 2026

Anthropic's Claude models Opus 4.6+ and Fable 5 no longer support last-assistant-turn prefills for forcing JSON output, returning a 400 error. Developers must now use structured outputs via `output_config.format` with Zod validation or tool use with `strict: true` for reliable schema enforcement. The choice depends on whether the JSON is the answer (structured outputs) or arguments to an action (tool use).

read3 min views1 publishedJul 8, 2026

For a long time the trick to force a model into JSON was to prefill the assistant turn with an opening brace. That trick is dead on the current Claude models. It returns a 400. If you are still doing it, your code breaks the moment you bump the model string. Here is the modern way to get reliable structured output, and how to pick between the two approaches that replaced prefills.

The old pattern:

// WRONG on Opus 4.6+, Fable 5: prefilling the last assistant turn now 400s
messages: [
  { role: "user", content: "Extract the name." },
  { role: "assistant", content: '{"name": "' }, // forced JSON start
]

This returned a 400 the first time I tried it on Opus 4.8. Last-assistant-turn prefills are no longer supported on Opus 4.6, 4.7, 4.8, or Fable 5. The replacement depends on what the prefill was doing.

If the prefill was forcing a JSON schema, the direct replacement is output_config.format

. The SDK validates the response against your schema automatically, and with Zod you get type safety for free:

import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";

const Finding = z.object({
  vulnerability: z.string(),
  severity: z.enum(["low", "medium", "high", "critical"]),
  function_name: z.string(),
  line: z.number(),
});

const response = await client.messages.parse({
  model: "claude-opus-4-8",
  max_tokens: 16000,
  messages: [{ role: "user", content: `Find the bug:\n\n${contract}` }],
  output_config: { format: zodOutputFormat(Finding) },
});

// Typed and validated. null if parsing failed.
console.log(response.parsed_output?.severity);

The output is guaranteed to match the schema or parsed_output

is null. No regex, no JSON.parse

in a try/catch, no "the model added a markdown fence again."

A few schema limits to know: numerical constraints like minimum

/maximum

are not enforced server-side (the SDK strips them and validates client-side), recursive schemas are not supported, and every object needs additionalProperties: false

. The Zod helper handles most of this for you.

If what you really want is for the model to do something with structured arguments, a tool is the cleaner fit. The classic case is classification: you want a label, and the label is the tool input.

const response = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  tools: [{
    name: "classify_severity",
    description: "Record the severity classification of a finding.",
    strict: true,
    input_schema: {
      type: "object",
      properties: {
        severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
      },
      required: ["severity"],
      additionalProperties: false,
    },
  }],
  tool_choice: { type: "tool", name: "classify_severity" },
  messages: [{ role: "user", content: `Classify:\n\n${finding}` }],
});

With strict: true

the arguments are guaranteed valid against the schema. Forcing tool_choice

to the specific tool means the model has to call it.

The deciding question: is the JSON the answer, or is the JSON the arguments to an action?

output_config.format

).For my auditor, the report is the answer, so I use structured outputs. For the routing logic that decides which analysis pass to run, the decision is an action, so I use a tool.

Prefills were used for more than JSON. Here is the mapping:

The prefill hack worked by exploiting how the model continues text. The modern features work by constraining the output format at the API level, which is both more reliable and impossible to break with a model bump. If you are still prefilling braces, you are one model-string change away from a 400. Move to structured outputs or tool use and stop fighting the formatter.

── more in #large-language-models 4 stories · sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/structured-outputs-v…] indexed:0 read:3min 2026-07-08 ·