CH NEO-ZÜRICH EDITION
WEATHER · OVERCAST 13°C
BLEND OF THE DAY · 07/ROGUE
EST. 2027
THE AEC CYBER MORNING NEWS

PAZ Kaffi

DESIGN · DEMOLITION · CAFFEINE · DISPATCH
EDITION 0919 · 19 September 2026
BROADCAST 04:42 CET
2,400 BROADSHEETS PRINTED
READ TIME · 47 MIN
Delete the retry wrapper — let Anthropic's API constrain the JSON shape
ACADEMY
FRAME · 06:50
19-09-2026

Delete the retry wrapper — let Anthropic's API constrain the JSON shape

Structured output makes an LLM return data you can load, not prose you parse. Anthropic's output_config constrains the shape — then you check the values.

On 14 September 2026, the benchmarking outfit BenchLM.ai ran a structured-output test and reported a number worth taping to your monitor: GPT-5.6 Terra returned valid JSON on all 87 completed calls and still got 10 of them wrong; Claude Sonnet 5 returned valid JSON on 86 of 87 and got 2 wrong. Different scores, identical lesson — neither provider claims a schema makes an answer true. Hold that gap. It is the whole reason this piece exists, and the reason the file you are about to delete was never going to be enough on its own.

You know the file. You pasted a paragraph of a Baubeschrieb into a model, asked for a room schedule, and got prose. You added “respond only with JSON” and got JSON wrapped in a code fence. You stripped the fence and got a trailing comma. You wrote a try/except json.JSONDecodeError with a retry, then a second retry, then a fence stripper. Six weeks later nobody on the team wants to touch it. That file is the thing to delete.

The Tool: the one thing this tutorial is built on is the official Anthropic Python SDK (pip install anthropic) — the same client.messages surface you already call, plus two methods that do the parsing you have been hand-rolling. It is worth an architect’s afternoon because it turns “model returns text you hope is JSON” into “model returns data your scheduler can load,” and it does it for room programmes, cost lines, or any Archicad automation job that ends in a table.

The mechanism is straightforward. You attach a JSON Schema through output_config={"format": schema} on messages.create(), and the response is constrained to that shape. Better still, client.messages.parse() validates the response against your schema for you — the retry wrapper’s job, moved server-side and into the SDK. For the other half of the problem, when the model should call something rather than return something, you set strict: true as a top-level field on the tool definition (beside name / description / input_schema) — not on tool_choice — with additionalProperties: false and required filled in. Then tool_use.input validates exactly.

If you learned this trick in 2024, retire one habit first. The assistant prefill — seeding the assistant turn with an opening { so the model had to continue in JSON — no longer works. It returns a 400 on Opus 5, Sonnet 5 and the 4.6/4.7/4.8 family. The older top-level output_format parameter is likewise deprecated; the current shape is output_config: {format: ...}. Naming the dead trick and its replacement in the same breath is the most useful thing this piece can do for anyone who last touched the API two years ago.

Setup:

python -m venv .venv
.venv\Scripts\activate        # Windows  (source .venv/bin/activate on macOS/Linux)
pip install anthropic
setx ANTHROPIC_API_KEY "sk-ant-..."   # export ANTHROPIC_API_KEY=... on macOS/Linux
python -c "import anthropic; print(anthropic.__version__)"

First steps:

  1. Define the schema — an object with a rooms array; each room carries name (string), area_m2 (number) and floor (string); set additionalProperties: false and mark all three required.
  2. Make the call with messages.parse() so the SDK validates for you. Default to claude-opus-5; a short extraction like this is exactly the shape where claude-haiku-4-5 is defensible on cost ($1.00 / $5.00 per million tokens in / out, against $5.00 / $25.00 for Opus 5, with Sonnet 5 between them at $2.00 / $10.00) — that is a note, not a rule; model choice is yours.
  3. Read the result: remember response.content is a list of content blocks, not a string — check block.type before you touch block.text. Do not lowball max_tokens; a truncated answer is a retry you pay for twice. ~16000 is a sane non-streaming default; a pure classification can sit near ~256.
import anthropic
client = anthropic.Anthropic()

schema = {
  "type": "object", "additionalProperties": False, "required": ["rooms"],
  "properties": {"rooms": {"type": "array", "items": {
    "type": "object", "additionalProperties": False,
    "required": ["name", "area_m2", "floor"],
    "properties": {
      "name":    {"type": "string"},
      "area_m2": {"type": "number"},
      "floor":   {"type": "string"}}}}}}

spec = "Erdgeschoss: Wohnraum 32 m2, Kueche 12 m2, WC 4 m2."

msg = client.messages.parse(
    model="claude-opus-5",
    max_tokens=16000,
    output_config={"format": schema},
    messages=[{"role": "user", "content": spec}],
)

←TODAY: Sept 2026 — valid JSON is now a one-line guarantee; correct values still are not. →3012: the offices that wrote a content check beside every schema check never shipped a plausible-but-wrong room schedule into a tender. Fulcrum: the schema proves the container; only a number you already trust proves the contents.

Here is the paragraph that turns an SDK tutorial into a PAZ piece. Schema-valid is not the same as correct. A response can satisfy every type in your schema and still say a room is 23 m² when the spec said 32. Structured output removes parse failures and exactly zero truth failures — which is precisely what BenchLM.ai measured: the container was perfect 87 times and the contents wrong 10 of them — and the model that scored better still missed two. This is the same discipline PAZ’s openBIM work keeps arriving at: a received model should be validated before it is trusted, not after it fails — buildingSMART’s IDS habit of checking a requirement automatically rather than by eye. A check that cannot fail is not a check.

Atelier: For a Swiss studio pushing programme prose into a schedule, the risk is not the crash you see — it is the clean-looking table nobody re-reads. The Monday move: wrap every extraction call so it prints the summed floor area beside the area you already hold on the plan, and refuses to write the row when they diverge. One assertion, added once, closes the exact gap BenchLM found. Turning that habit into a repeatable in-house workflow is the kind of hands-on skill PAZGPT and our PAZ workshops are built to drill.

Hack: Set the total you extracted next to the floor area you already trust, and refuse the result when they will not reconcile. The schema already guarantees the shape; this is the line that guards the meaning.

rooms = msg.parsed["rooms"]
extracted = sum(r["area_m2"] for r in rooms)
known_floor = 48.0                     # the number from the plan you already trust
print(f"extracted {extracted} m2  vs  known {known_floor} m2")
assert abs(extracted - known_floor) < 2.0, "areas don't reconcile — read the prose again"

That assert is the whole lesson. Steps one and two are the mechanics; step three — putting the number next to the number — is the part that survives the next model release, when the API surface has shifted again and the prefill trick is a fond memory.

Learn-it:

Delete the retry wrapper — but only after you have written the one line that checks the values, because that is the line the wrapper never had.

FILED FROM
CO-SIGNERS
PAZ Academy
CONFIDENCE
HIGH
REPRINTS
© PAZ - PARAMETRIC ACADEMY ZURICH · ALL RIGHTS RESERVED

PAZ Kaffi · multidisciplinary editorial, led by PAZ Academy

⚑ REPORT AN ERROR · SUBMIT A CORRECTION
◂ BACK TO FRONT PAGE · PAZ KAFFI

© 2026 PAZ Academy.