Skip to main content

Structured Output Guide

Get predictable, typed responses from Claude using Zod schemas with automatic validation and retry.

Basic Schema

import { z } from "zod";

const ResultSchema = z.object({
  success: z.boolean(),
  message: z.string(),
});

<Claude
  schema={ResultSchema}
  onFinished={(result) => {
    // result.structured is typed!
    console.log(result.structured.success);
    console.log(result.structured.message);
  }}
>
  Complete the task.
</Claude>

Schema with Descriptions

Descriptions help Claude understand what each field should contain:
const AnalysisSchema = z.object({
  summary: z.string().describe("A 1-2 sentence summary of the findings"),
  confidence: z.number().min(0).max(1).describe("Confidence level from 0 to 1"),
  issues: z.array(z.object({
    severity: z.enum(["low", "medium", "high", "critical"])
      .describe("Impact level of this issue"),
    description: z.string().describe("Clear description of the issue"),
    location: z.string().describe("File path and line number"),
  })).describe("List of issues found, ordered by severity"),
});

Enums and Literals

const StatusSchema = z.object({
  status: z.enum(["success", "failure", "pending"]),
  errorCode: z.literal("E001").optional(),
  category: z.union([
    z.literal("bug"),
    z.literal("feature"),
    z.literal("refactor"),
  ]),
});

Arrays and Objects

const ReportSchema = z.object({
  // Simple array
  tags: z.array(z.string()),

  // Array of objects
  findings: z.array(z.object({
    type: z.string(),
    message: z.string(),
  })),

  // Nested objects
  metadata: z.object({
    author: z.string(),
    timestamp: z.string(),
    version: z.number(),
  }),

  // Record type
  scores: z.record(z.string(), z.number()),
});

Optional and Default Values

const ConfigSchema = z.object({
  name: z.string(),
  description: z.string().optional(),
  enabled: z.boolean().default(true),
  maxRetries: z.number().default(3),
});

Validation and Retry

Smithers automatically retries if validation fails:
<Claude
  schema={AnalysisSchema}
  maxRetries={2}  // Retry up to 2 times on validation failure
  retryOnValidationFailure  // Default: true
  onFinished={(result) => {
    // Guaranteed to match schema
  }}
  onError={(err) => {
    // Called after all retries exhausted
    console.error("Validation failed:", err);
  }}
>
  Analyze the code.
</Claude>

Custom Validation

Add business logic validation:
<Claude
  schema={AnalysisSchema}
  validate={(result) => {
    // Must have at least one finding
    if (result.issues.length === 0) {
      return false;
    }
    // Score must be reasonable
    if (result.score < 0 || result.score > 100) {
      return false;
    }
    return true;
  }}
  maxRetries={2}
>
  Find issues in the code.
</Claude>

TypeScript Integration

Full type inference:
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  role: z.enum(["admin", "user", "guest"]),
});

// Infer the type
type User = z.infer<typeof UserSchema>;

<Claude<User>
  schema={UserSchema}
  onFinished={(result) => {
    const user: User = result.structured!;
    // Fully typed access
    console.log(user.name);
    console.log(user.email);
    console.log(user.role);  // "admin" | "user" | "guest"
  }}
>

Complex Real-World Example

const CodeReviewSchema = z.object({
  summary: z.string().describe("Executive summary of the review"),

  verdict: z.enum(["approve", "request_changes", "comment"])
    .describe("Overall review verdict"),

  issues: z.array(z.object({
    severity: z.enum(["critical", "major", "minor", "nitpick"]),
    category: z.enum(["security", "performance", "maintainability", "correctness", "style"]),
    file: z.string(),
    line: z.number().optional(),
    title: z.string().max(100),
    description: z.string(),
    suggestion: z.string().optional(),
    autoFixable: z.boolean().default(false),
  })).describe("Issues found during review"),

  positives: z.array(z.string())
    .describe("Good practices observed"),

  metrics: z.object({
    linesReviewed: z.number(),
    filesReviewed: z.number(),
    complexityScore: z.number().min(1).max(10),
  }),

  nextSteps: z.array(z.string())
    .describe("Recommended actions"),
});

<Claude
  model="opus"
  schema={CodeReviewSchema}
  maxRetries={3}
  allowedTools={["Read", "Glob", "Grep"]}
  onFinished={(result) => {
    const review = result.structured!;

    if (review.verdict === "approve") {
      console.log("✅ Review approved!");
    } else {
      console.log(`❌ ${review.issues.length} issues found`);
      for (const issue of review.issues) {
        console.log(`  [${issue.severity}] ${issue.title}`);
      }
    }
  }}
>
  Review the changes in this PR comprehensively.
</Claude>

Prompt Tips

Help Claude produce valid output:
<Claude schema={Schema}>
  Analyze the code and return a structured response.

  Important:
  - All required fields must be present
  - Severity must be one of: low, medium, high, critical
  - Score must be between 0 and 100
  - Include at least one recommendation
</Claude>

Handling Errors

<Claude
  schema={Schema}
  onFinished={(result) => {
    if (!result.structured) {
      console.error("No structured output despite validation passing");
      return;
    }
    // Use result.structured safely
  }}
  onError={(error) => {
    if (error.message.includes("validation")) {
      console.error("Schema validation failed after retries");
    } else {
      console.error("Execution error:", error);
    }
  }}
>