{"slug": "claude-code-structured-output-mode-in-2026-forcing-json-responses-from-subagents", "title": "Claude Code Structured Output Mode in 2026: Forcing JSON Responses From Subagents and Validating Them With Zod", "summary": "A developer documented how Claude Code's structured output mode forces subagents to return schema-validated JSON instead of prose, using Zod schemas converted to JSON Schema and passed as an outputFormat to startSubagent(). The approach, demonstrated on Claude Code SDK v0.3.301 with Claude Fable 5.1, wraps a subagent's final response in a synthetic tool call validated against the schema, with a SubagentStop hook catching violations before they reach the lead agent's context. The writeup notes that invalid schemas now fail at startup as of SDK v2.1.205, and that failed validations surface as error_max_structured_output_retries after a default of three attempts.", "body_md": "*This article was written with the assistance of AI, under human supervision and review.*\n\nSubagents in Claude Code return unstructured text by default. When a lead agent spawns five parallel subagents to extract metadata from API endpoints, the lead receives five prose summaries. Parsing those summaries for the actual data means regex patterns, fallback heuristics, and silent failures when a subagent changes its response format mid-conversation.\n\n*Problem flow showing subagent returning unstructured text that the lead must parse*\n\nStructured output mode forces subagents to return validated JSON that matches a schema you define. The lead receives the exact shape it expects, and Zod validates every field before the result reaches the lead's context. No parsing, no silent failures.\n\n*Solution flow showing subagent returning validated JSON schema*\n\nThis post shows how to pass `outputFormat` to subagent sessions, validate structured output with Zod before results surface to the lead, and compose JSON schemas for multi-agent workflows that demand reliability. Examples run on Claude Code SDK v0.3.301 with Claude Fable 5.1 (`claude-fable-5-1`), which dropped support for `tool_choice` types `any` and `tool` in the September 1, 2026 release. The structured output and strict tool use patterns here replace the removed tool-choice guarantees.\n\n*Claude Code structured output validation workflow*\n\n`outputFormat: { type: \"json_schema\", schema: zodSchema }` to `startSubagent()` to constrain a subagent's final result.`.toJSONSchema(schema, { target: \"draft-7\" })` and provide TypeScript types through inference.`SubagentStop` hook fires before a subagent's result reaches the lead, so Zod validation there catches schema violations before context pollution.`additionalContext`.\nStructured output mode wraps the subagent's final response in a synthetic tool call. The SDK generates a tool definition from the schema you provide, instructs the model to call that tool with the result, validates the call against the schema, and surfaces the validated JSON to the lead. The subagent still uses any tools it needs during execution (Grep, Bash, file operations), but the final turn must conform to the structure you defined.\n\n*Conceptual flow of structured output mode transforming subagent result into synthetic tool call*\n\nThe schema is a JSON Schema object. Zod schemas convert to JSON Schema with `zodToJsonSchema()` from the `zod-to-json-schema` package. The SDK supports standard JSON Schema features: object, array, string, number, boolean, null, enum, const, required, nested objects, and `$ref` definitions. The `format` keyword (like `\"format\": \"email\"`) is accepted as an annotation but not enforced. Before SDK v2.1.205, invalid schemas were silently ignored and the subagent returned unstructured text. After v2.1.205, an invalid schema fails the run at startup with an error naming the problem.\n\nThe subagent attempts the synthetic tool call up to the retry limit (default 3). If validation fails on every attempt, the result becomes an error of type `error_max_structured_output_retries`. The lead sees the error in the subagent's summary unless your `SubagentStop` hook intercepts it first. Production workflows often validate in the hook and handle the error there, so the lead never receives a malformed payload.\n\nA subagent session accepts `outputFormat` in the same shape as the lead's `query()` call. Define a Zod schema, convert it to JSON Schema, and pass it to `startSubagent()`. The example below spawns a subagent to extract repository metadata and return it as a typed object:\n\n``` js\nimport { z } from \"zod\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\nimport { startSubagent } from \"@anthropic-ai/claude-agent-sdk\";\n\nconst repoMetadataSchema = z.object({\n  name: z.string().describe(\"Repository name\"),\n  primaryLanguage: z.string().describe(\"Primary programming language\"),\n  stars: z.number().int().nonnegative().describe(\"Star count\"),\n  openIssues: z.number().int().nonnegative().describe(\"Open issue count\"),\n  lastCommitDate: z.string().describe(\"ISO 8601 date of last commit\")\n});\n\ntype RepoMetadata = z.infer<typeof repoMetadataSchema>;\n\nconst jsonSchema = zodToJsonSchema(repoMetadataSchema, { \n  target: \"draft-7\" \n});\n\nasync function analyzeRepository(repoPath: string): Promise<RepoMetadata> {\n  const messages = [];\n\n  for await (const message of startSubagent({\n    prompt: `Analyze the repository at ${repoPath} and extract metadata`,\n    options: {\n      outputFormat: {\n        type: \"json_schema\",\n        schema: jsonSchema\n      }\n    }\n  })) {\n    messages.push(message);\n\n    if (message.type === \"result\" && message.subtype === \"success\") {\n      if (!message.structured_output) {\n        throw new Error(\"Expected structured output but received none\");\n      }\n      return message.structured_output as RepoMetadata;\n    }\n\n    if (message.type === \"result\" && message.subtype === \"error\") {\n      throw new Error(`Subagent failed: ${message.error}`);\n    }\n  }\n\n  throw new Error(\"Subagent completed without result\");\n}\n```\n\nThe subagent uses Bash to run `git log`, Grep to search for language indicators, and Read to examine configuration files. The final result matches the schema or triggers a retry. The lead receives a typed `RepoMetadata` object with no parsing layer.\n\nOptional fields follow Zod's `.optional()` pattern. The JSON Schema includes them in `properties` but omits them from `required`. When the subagent cannot populate an optional field (like an author name for a file with no git history), it omits the key from the synthetic tool call, and TypeScript infers the field as `string | undefined`:\n\n``` js\nconst todoSchema = z.object({\n  text: z.string(),\n  file: z.string(),\n  line: z.number().int().positive(),\n  author: z.string().optional(),\n  date: z.string().optional()\n});\n\ntype Todo = z.infer<typeof todoSchema>;\n```\n\nThe `author` and `date` keys appear in the JSON Schema with no required constraint. The subagent populates them when git blame data exists and omits them otherwise. Downstream code checks `todo.author !== undefined` before using the value.\n\nZod provides both the JSON Schema for the SDK and the runtime validator for the result. The `parse()` method throws on validation failure. The `safeParse()` method returns `{ success: true, data: T }` or `{ success: false, error: ZodError }`. Production code often uses `safeParse()` in a `SubagentStop` hook to catch malformed output before it reaches the lead.\n\nThe schema from the previous section generates this JSON Schema, which the SDK passes to the model:\n\n```\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Repository name\"\n    },\n    \"primaryLanguage\": {\n      \"type\": \"string\",\n      \"description\": \"Primary programming language\"\n    },\n    \"stars\": {\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"description\": \"Star count\"\n    },\n    \"openIssues\": {\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"description\": \"Open issue count\"\n    },\n    \"lastCommitDate\": {\n      \"type\": \"string\",\n      \"description\": \"ISO 8601 date of last commit\"\n    }\n  },\n  \"required\": [\"name\", \"primaryLanguage\", \"stars\", \"openIssues\", \"lastCommitDate\"],\n  \"additionalProperties\": false\n}\n```\n\nRuntime validation applies the same constraints. Zod checks that `stars` is a nonnegative integer and `lastCommitDate` is a string. Custom refinements enforce stricter rules:\n\n``` js\nconst repoMetadataSchema = z.object({\n  name: z.string().min(1),\n  primaryLanguage: z.string().min(1),\n  stars: z.number().int().nonnegative(),\n  openIssues: z.number().int().nonnegative(),\n  lastCommitDate: z.string().refine(\n    (val) => !isNaN(Date.parse(val)),\n    { message: \"Must be a valid ISO 8601 date\" }\n  )\n});\n```\n\nThe refinement on `lastCommitDate` rejects malformed date strings like `\"2026-13-01\"`. The SDK's validation catches basic type mismatches, and Zod's refinement catches semantic errors. When a subagent returns `\"2026-13-01\"`, the synthetic tool call fails validation, the SDK retries, and the subagent eventually corrects itself or exhausts retries.\n\n*Zod validation pipeline from JSON Schema to runtime type checking*\n\nZod v4 (released in late 2025) changed the refinement API and the shape of `ZodError`. The `zodToJsonSchema()` function supports both Zod v3 and v4. Projects upgrading to Zod v4 must update error-handling code that inspects `error.issues` directly, because the `issues` array structure changed. The JSON Schema output remains compatible across versions.\n\nThe `SubagentStop` hook fires when a subagent finishes and before its summary reaches the lead. A hook firing in SDK v0.3.301+ receives `hookSpecificOutput.additionalContext`, so validation failures can extend the subagent's turn with correction prompts instead of terminating the subagent outright. The hook returns `behavior: \"cancel\"` to reject the result or `behavior: \"allow\"` to forward it to the lead.\n\n*Execution flow showing SubagentStop hook validating output before lead receives it*\n\nThe example below validates repository metadata in the hook and extends the subagent's turn on failure:\n\n``` js\nimport { z } from \"zod\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\nimport { query } from \"@anthropic-ai/claude-agent-sdk\";\n\nconst repoMetadataSchema = z.object({\n  name: z.string().min(1),\n  primaryLanguage: z.string().min(1),\n  stars: z.number().int().nonnegative(),\n  openIssues: z.number().int().nonnegative(),\n  lastCommitDate: z.string().refine(\n    (val) => !isNaN(Date.parse(val)),\n    { message: \"Must be a valid ISO 8601 date\" }\n  )\n});\n\ntype RepoMetadata = z.infer<typeof repoMetadataSchema>;\n\nconst jsonSchema = zodToJsonSchema(repoMetadataSchema, { \n  target: \"draft-7\" \n});\n\nasync function analyzeWithValidation(repoPath: string): Promise<RepoMetadata> {\n  for await (const message of query({\n    prompt: `Spawn a subagent to analyze the repository at ${repoPath}`,\n    options: {\n      subagentStop: async (payload) => {\n        const output = payload.hookSpecificOutput.structuredOutput;\n\n        if (!output) {\n          return {\n            behavior: \"cancel\",\n            additionalContext: \"No structured output received\"\n          };\n        }\n\n        const result = repoMetadataSchema.safeParse(output);\n\n        if (!result.success) {\n          const errors = result.error.issues\n            .map(issue => `${issue.path.join(\".\")}: ${issue.message}`)\n            .join(\"; \");\n\n          return {\n            behavior: \"allow\",\n            additionalContext: `Validation failed: ${errors}. Correct the output and try again.`\n          };\n        }\n\n        return { behavior: \"allow\" };\n      }\n    }\n  })) {\n    if (message.type === \"result\" && message.subtype === \"success\") {\n      return message.structured_output as RepoMetadata;\n    }\n  }\n\n  throw new Error(\"Query completed without result\");\n}\n```\n\nThe hook receives `hookSpecificOutput.structuredOutput` containing the subagent's JSON output before the lead sees it. Validation failures produce a correction prompt in `additionalContext`, and the subagent continues with the feedback. The hook returns `behavior: \"allow\"` so the subagent remains active. When validation passes, the hook returns `behavior: \"allow\"` with no `additionalContext`, and the lead receives the validated result.\n\nThe pattern scales to multiple subagents. A lead spawning ten parallel subagents passes the same `subagentStop` hook to all of them. Each subagent's output validates independently, and only valid results reach the lead. Invalid results trigger correction turns inside the subagent, invisible to the lead until the subagent produces valid output or exhausts retries.\n\nThe SDK retries failed validation up to the retry limit (default 3). After the limit, the result becomes an error of type `error_max_structured_output_retries`. The lead sees this error in the subagent's summary unless the `SubagentStop` hook cancels the subagent first. Production workflows typically catch the error in the hook and log it for debugging, then cancel the subagent so the lead does not receive a broken result.\n\n*Decision tree showing retry loop and eventual failure path for invalid structured output*\n\nThe retry loop is automatic. The SDK provides feedback to the model after each failed attempt, describing the validation error and the expected schema. The model corrects its response and tries again. When retries exhaust without a valid result, the error surfaces. The example below catches the error in the main query loop:\n\n```\nasync function analyzeWithErrorHandling(repoPath: string): Promise<RepoMetadata | null> {\n  try {\n    for await (const message of query({\n      prompt: `Spawn a subagent to analyze the repository at ${repoPath}`,\n      options: {\n        subagentStop: async (payload) => {\n          const output = payload.hookSpecificOutput.structuredOutput;\n\n          if (!output) {\n            console.error(\"Subagent produced no structured output\");\n            return { behavior: \"cancel\" };\n          }\n\n          const result = repoMetadataSchema.safeParse(output);\n\n          if (!result.success) {\n            console.error(\"Validation failed:\", result.error.issues);\n            return { behavior: \"cancel\" };\n          }\n\n          return { behavior: \"allow\" };\n        }\n      }\n    })) {\n      if (message.type === \"result\" && message.subtype === \"success\") {\n        return message.structured_output as RepoMetadata;\n      }\n\n      if (message.type === \"result\" && message.subtype === \"error\") {\n        console.error(\"Subagent error:\", message.error);\n        return null;\n      }\n    }\n  } catch (error) {\n    console.error(\"Query threw:\", error);\n    return null;\n  }\n\n  return null;\n}\n```\n\nThe `SubagentStop` hook cancels the subagent when validation fails. The lead receives a result message with `subtype: \"error\"`, and the main loop returns `null`. Downstream code checks for `null` and handles the missing data accordingly.\n\nSome validation failures are unrecoverable. When the schema expects an integer and the subagent's tools can only return a string, retries will exhaust without progress. The hook pattern above logs the failure and terminates early, saving tokens and latency. Production systems often include a fallback path: if structured output fails, the lead spawns a second subagent with a simpler schema or a prose-based extraction prompt.\n\nMulti-agent workflows compose schemas hierarchically. A lead spawns a subagent to analyze a repository, and that subagent spawns child subagents to analyze individual files. Each level defines its own schema, and the parent's schema includes the child's schema as a nested object or array.\n\n*Multi-level workflow showing lead agent receiving composed results from nested subagents*\n\nThe repository metadata schema from earlier examples extends to include an array of file analyses:\n\n``` js\nconst fileAnalysisSchema = z.object({\n  path: z.string(),\n  language: z.string(),\n  lines: z.number().int().positive(),\n  complexity: z.number().int().nonnegative(),\n  lastModified: z.string()\n});\n\nconst repoAnalysisSchema = z.object({\n  name: z.string().min(1),\n  primaryLanguage: z.string().min(1),\n  stars: z.number().int().nonnegative(),\n  openIssues: z.number().int().nonnegative(),\n  lastCommitDate: z.string(),\n  files: z.array(fileAnalysisSchema)\n});\n\ntype RepoAnalysis = z.infer<typeof repoAnalysisSchema>;\n```\n\nThe lead spawns a subagent with `outputFormat` set to the `repoAnalysisSchema`. That subagent spawns child subagents to analyze individual files, each with `outputFormat` set to `fileAnalysisSchema`. The lead receives a single `RepoAnalysis` object with fully typed nested data. Validation at each level guarantees that every file analysis conforms to `fileAnalysisSchema` and the repository analysis conforms to `repoAnalysisSchema`.\n\nThe composition pattern scales to arbitrary nesting depth. A subagent analyzing a file might spawn subagents to analyze functions, and those subagents return function-level schemas. The top-level lead receives a deeply nested typed object with no manual assembly.\n\nParallel subagents often return arrays. A lead spawning ten subagents to analyze ten repositories defines a schema like this:\n\n``` js\nconst batchAnalysisSchema = z.object({\n  repositories: z.array(repoAnalysisSchema),\n  totalStars: z.number().int().nonnegative(),\n  mostActiveRepo: z.string()\n});\n```\n\nThe lead spawns ten child subagents, each returning a `RepoAnalysis` object. The lead collects the results, computes `totalStars` and `mostActiveRepo`, and returns a `batchAnalysisSchema`-shaped object. The parent that spawned the lead receives a single typed payload summarizing ten repositories.\n\nComparison of schema composition strategies:\n\n| Strategy | Use case | Validation overhead | Type safety | \n|---|---|---|---|\n| Flat schema | Single subagent, simple output | Low (one validation) | Full (single-level inference) | \n| Nested schema | Hierarchical analysis (repo → files) | Medium (validation per level) | Full (deep inference with nested types) | \n| Array schema | Parallel subagents returning same shape | High (validation per element) | Full (array element type inferred) | \n| Discriminated union | Subagents returning different shapes | Medium (one validation, branching types) | Full (union type inferred) | \n\nDiscriminated unions handle cases where subagents return different shapes based on runtime conditions. A subagent analyzing a file might return different schemas for source files versus configuration files:\n\n``` js\nconst sourceFileSchema = z.object({\n  type: z.literal(\"source\"),\n  path: z.string(),\n  language: z.string(),\n  functions: z.array(z.string())\n});\n\nconst configFileSchema = z.object({\n  type: z.literal(\"config\"),\n  path: z.string(),\n  format: z.string(),\n  keys: z.array(z.string())\n});\n\nconst fileSchema = z.discriminatedUnion(\"type\", [\n  sourceFileSchema,\n  configFileSchema\n]);\n\ntype FileAnalysis = z.infer<typeof fileSchema>;\n```\n\nThe subagent returns a `type` field that determines the shape. TypeScript narrows the type based on the discriminant:\n\n```\nfunction processFile(file: FileAnalysis) {\n  if (file.type === \"source\") {\n    console.log(`Functions: ${file.functions.join(\", \")}`);\n  } else {\n    console.log(`Config keys: ${file.keys.join(\", \")}`);\n  }\n}\n```\n\nThis pattern is common in workflows where a subagent inspects a file and decides the appropriate analysis strategy at runtime. The lead receives a single schema that accommodates both outcomes, and TypeScript enforces exhaustive handling of the union.\n\nThe SDK yields a result message with `type: \"result\"`, `subtype: \"error\"`, and `error` containing `error_max_structured_output_retries`. If your `SubagentStop` hook cancels the subagent on validation failure, the lead receives an error result and can handle it with a fallback strategy. If the hook does not cancel, the lead sees the retry-exhaustion error in the subagent's summary.\n\nStructured output mode wraps the final result in a synthetic tool call, separate from any custom tools the subagent uses during execution. The subagent can call custom tools (like a database query tool) as many times as needed, then return the final result via the synthetic tool. Custom tools do not need to match the output schema; only the final result does.\n\nReplace the prose prompt (like \"return the data as JSON\") with an `outputFormat` option. Define a Zod schema matching the shape your regex currently extracts, convert it to JSON Schema with `zodToJsonSchema()`, and pass it to `startSubagent()`. Remove the regex parsing code and read `message.structured_output` directly. Add a `SubagentStop` hook to validate the output with Zod before it reaches the lead, catching any edge cases your regex missed.\n\nStructured output mode adds one synthetic tool call at the end of the subagent's execution. Latency increases by the time to generate and validate that call (typically 100-300ms for Claude Fable 5.1). Retries on validation failure add incremental latency, but the retry loop is automatic and parallel across multiple subagents, so ten parallel subagents with retries often complete faster than sequential prose parsing.\n\nZod v4 refinements work with the SDK because `zodToJsonSchema()` supports both Zod v3 and v4. The refinement runs at runtime in your `SubagentStop` hook or result handler, not inside the SDK's validation. The SDK validates basic types against the JSON Schema, and Zod's `safeParse()` applies refinements like date format checks or custom business logic.\n\nStructured output mode in Claude Code eliminates regex parsing from multi-agent workflows. Subagents return validated JSON matching schemas you define, and Zod enforces type safety from the schema definition through runtime validation. The `SubagentStop` hook integrates validation before results reach the lead, so invalid payloads trigger correction turns inside the subagent or terminate early. Production pipelines compose schemas hierarchically, spawning subagents that return typed objects fitting directly into the parent's schema. Related patterns for cost control and CI integration appear in [Claude Code cost control for production workflows](https://jsmanifest.com/claude-code-cost-control-production) and [CI agentic review with auto-fix](https://jsmanifest.com/claude-code-ci-agentic-review-test-auto-fix). Compare the 2026 tooling landscape in [Claude Code versus Cursor and Windsurf](https://jsmanifest.com/claude-code-cursor-windsurf-production-2026).", "url": "https://wpnews.pro/news/claude-code-structured-output-mode-in-2026-forcing-json-responses-from-subagents", "canonical_source": "https://dev.to/jsmanifest/claude-code-structured-output-mode-in-2026-forcing-json-responses-from-subagents-and-validating-3na4", "published_at": "2026-10-29 12:06:22+00:00", "updated_at": "2026-10-29 12:17:38.814319+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models"], "entities": ["Claude Code", "Anthropic", "Zod", "Claude Fable 5.1", "Claude Code SDK"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-code-structured-output-mode-in-2026-forcing-json-responses-from-subagents", "markdown": "https://wpnews.pro/news/claude-code-structured-output-mode-in-2026-forcing-json-responses-from-subagents.md", "text": "https://wpnews.pro/news/claude-code-structured-output-mode-in-2026-forcing-json-responses-from-subagents.txt", "jsonld": "https://wpnews.pro/news/claude-code-structured-output-mode-in-2026-forcing-json-responses-from-subagents.jsonld"}}