0Pricing
Claude Architect · Lesson

Structured Output

--output-format json with a schema for parsing.

Structured Output is a free Claude Architect lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Claude Architect learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Structured Output in CI/CD

When you run Claude Code inside a pipeline, a human is not reading the result — a script is. A CI job needs a stable, machine-parseable answer so it can fail the build, post a comment, or gate a merge.

Two flags make this possible:

  • -p (or --print) runs Claude Code non-interactively — required in any pipeline.
  • --output-format json returns a parseable result instead of free-form text.

This lesson is Domain 4 (Prompt Engineering & Structured Output) meeting Scenario 5 (Claude Code for CI/CD).

claude -p "Review the staged diff for security bugs" \
  --output-format json

The Problem With Free-Form Text

If you let the model answer in prose, your pipeline has to scrape that prose with regexes — counting words, hunting for phrases like "looks good" or "found issues". That is brittle and a classic anti-pattern.

The same rule applies to the agentic loop: you terminate on a stop_reason, never by parsing text for words like "done". In CI, you decide pass/fail from structured fields, never from free text.

Structured output replaces fragile text-scraping with a contract your script can trust.

Adding a Schema

--output-format json gives you JSON, but plain JSON can still vary in shape. Pair it with a JSON Schema so the output always has the exact fields your pipeline expects.

A schema delivers two guarantees:

  • It eliminates syntax errors — no half-formed JSON to crash your parser.
  • It enforces required fields — the fields you mark required are always present.

Schema-constrained output is the same mechanism that powers tool use: tool_use + JSON Schema is how Claude returns reliable structured data.

{
  "type": "object",
  "properties": {
    "verdict": { "type": "string", "enum": ["pass", "fail"] },
    "issues": {
      "type": "array",
      "items": { "type": "object" }
    }
  },
  "required": ["verdict", "issues"]
}

Designing the Issue Object

Make each finding a precise object the pipeline can act on. A good review schema gives every issue a location, a severity, and an explanation — so the job can annotate the exact line.

Use an enum for severity to keep values consistent across runs. Free-text severities like "kinda bad" are unparseable.

{
  "type": "object",
  "properties": {
    "file":     { "type": "string" },
    "line":     { "type": "integer" },
    "severity": { "type": "string",
                   "enum": ["blocker", "major", "minor"] },
    "message":  { "type": "string" }
  },
  "required": ["file", "severity", "message"]
}

Required Fields: The Golden Rule

Mark a field required only if it is always present. This is the single most-tested structured-output rule.

If you require a field that may be absent — say line for a project-wide finding that has no specific line — the model will fabricate a value to satisfy the schema. That hallucinated line number then drives a wrong CI annotation.

In the previous scene, line was deliberately left out of required: not every issue maps to one line.

Enums + an "other" Escape Hatch

Enums keep values clean, but a rigid enum can box the model in when reality doesn't fit any category. The extensible pattern: add an "other" enum value plus a free-text detail field.

Now the model can stay in-schema for the common cases and still report the unexpected one without fabricating a wrong category.

{
  "category": {
    "type": "string",
    "enum": ["security", "performance",
             "style", "other"]
  },
  "category_detail": {
    "type": "string",
    "description": "Free text when category is 'other'"
  }
}

Guaranteeing Structure With tool_choice

When you call Claude through the SDK rather than the CLI, you guarantee structured output by combining a tool whose input_schema is your JSON Schema with the right tool_choice:

  • "auto" — the model may answer in text OR call a tool (no guarantee).
  • "any" — the model MUST call some tool, which guarantees structured output.
  • {"type":"tool","name":"X"} — forces one specific tool.

For a CI review where you always need the report object, force the exact tool by name.

resp = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=2048,
    tools=[review_report_tool],   # input_schema = your JSON Schema
    tool_choice={"type": "tool", "name": "emit_review"},
    messages=[{"role": "user", "content": diff_text}],
)

Parsing the CI Result

In the pipeline you read the JSON, then branch on a field — never on the prose. Pull the structured payload and let the verdict decide the exit code.

Because the schema marked verdict and issues as required, this code never has to guess whether the keys exist.

import json, subprocess, sys

out = subprocess.run(
    ["claude", "-p", PROMPT, "--output-format", "json"],
    capture_output=True, text=True,
).stdout

report = json.loads(out)
if report["verdict"] == "fail":
    for i in report["issues"]:
        print(f"{i['file']}:{i.get('line','-')} {i['message']}")
    sys.exit(1)

Validate, Then Retry With Feedback

Even with a schema, a value can be semantically wrong (a bad arithmetic total, a malformed reference). Validate the parsed object with Pydantic-style checks, and on a structural/format error use retry-with-feedback.

Send the model three things: the original input, the wrong output it produced, and the exact validation error. This fixes format, structural, and arithmetic mistakes.

Key limit: retry does NOT help when the information is simply absent from the source — no amount of re-prompting invents data that isn't there.

from pydantic import BaseModel, ValidationError

class Review(BaseModel):
    verdict: str
    issues: list[dict]

try:
    review = Review.model_validate_json(out)
except ValidationError as e:
    retry(original=diff_text, bad_output=out, error=str(e))

Review in an Isolated Session

If the same conversation that generated code also reviews it, the reviewer keeps its own reasoning and won't challenge itself — same-session self-review is an anti-pattern.

Run the structured review in an isolated, fresh session. An independent instance is far better at catching real defects. This pairs naturally with structured output: a clean session in, a clean JSON report out.

Tune the prompt with explicit criteria ("flag a comment only when it contradicts the code") to minimize false positives that would block good merges.

Blocking Checks vs. Overnight Audits

A pre-merge CI gate is blocking and time-sensitive — run it synchronously with -p --output-format json. Using the Message Batches API here is wrong: batches are 50% cheaper but have no latency SLA, up to a 24h window, and don't support multi-turn tool calling.

Reserve the Batch API for non-blocking jobs — an overnight audit of the whole repo, a nightly report — where custom_id correlates each request and you re-submit only failures.

Quick Check

Apply the structured-output rules to a real pipeline decision.

Recap: Structured Output in CI/CD

Key takeaways:

  • In pipelines run Claude Code with -p (non-interactive) and --output-format json plus a schema; branch on fields, never on prose.
  • A JSON Schema eliminates syntax errors and enforces required fields.
  • Mark a field required ONLY if it is always present — requiring a possibly-absent field causes fabrication.
  • Use enums with an "other" value + a detail field for extensibility.
  • Via the SDK, tool_choice "any" or a forced tool guarantees structured output; "auto" does not.
  • Validate (Pydantic-style) and retry-with-feedback (original + wrong output + exact error) for format errors — but retries can't supply absent data.
  • Review in an isolated/fresh session, not the generating one; minimize false positives with explicit criteria.
  • Blocking gate = synchronous; Batch API only for non-blocking overnight jobs.

Frequently asked questions

Is the “Structured Output” lesson free?

Yes — the full text of “Structured Output” is free to read here on the web, and the Claude Architect course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Claude Architect course, upgrade to CoddyKit PRO.

What will I learn in “Structured Output”?

--output-format json with a schema for parsing. You practise Claude Architect with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Claude Architect?

No prior experience is required. Claude Architect on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Structured Output” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Claude Architect lesson?

Yes. Every Claude Architect lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Non-Interactive Mode
  2. Structured Output
  3. Session Isolation for Reviews
  4. Test Generation & Standards
← Back to Claude Architect