Lesson
Enforcing Structured Output with JSON Schema and Zod in Claude Code workflows
Stop hoping for valid JSON—demand it. Learn to use JSON schemas with AI for structured output, then validate and type it in TypeScript using Zod.
- Access
- Included
- Transcript
- Needs source
Getting predictable, structured data from an AI can feel like a game of chance. When you need a specific format like JSON to drive an automation pipeline, you can't afford guesswork. This lesson demonstrates a powerful workflow to enforce structured output from AI models using JSON schemas, and then process that data with full type-safety in a local script.
You'll learn how to take complete control over both the AI's input and its output, enabling complex and reliable automation.
The Workflow
This lesson walks through a complete end-to-end process:
- Generate a JSON Schema: Use an AI agent (Claude Code in this case) to create a
tasks.schema.jsonfile that defines the exact structure for a list of tasks. - Prompt with the Schema: Embed the schema directly into your prompt using
<schema>tags. This instructs the AI to generate its response according to that specific structure. - Process the Structured Output: Pipe the AI's JSON output directly into a local script (using Bun and TypeScript) that can parse the incoming data from
stdin. - Add Type Safety: Use libraries like
json-schema-to-zodandzodto automatically generate TypeScript types from your JSON schema. This allows you to validate and process the data with full type-safety, catching errors and leveraging editor autocompletion. - Build a Pipeline: Combine these steps into a single, powerful command-line pipeline that goes from a high-level prompt to a type-safe, actionable object in your code.
Key Benefits
- Reliable AI Output: Drastically reduce variability by forcing the AI to adhere to a strict JSON structure.
- Powerful Automation: Create complex, multi-step workflows where AI-generated data is programmatically processed by local scripts or APIs.
- Full Type Safety: Eliminate runtime errors by validating data against generated Zod schemas and leveraging TypeScript's type system.
- Dynamic and Maintainable: Easily update your data structure by modifying the JSON schema and regenerating the types with a single command.
This approach transforms the AI from a simple text generator into a reliable component in your development toolchain.
AI Prompts
Generating the initial schema for tasks:
Create a schemas directory and generate a schema for tasks which is. Is simply an array where each task has a name and a description.
Installing dependencies and generating TypeScript types from the schema:
Please install a library which can convert a JSON schema into Zod types. We'll probably have to install Zod as well. Add that convert. Conversion logic to my package.json so that I can update it whenever the schema changes. And then in my index.ts, import the types and Type my tasks so that my tasks are typed to the schema properly.
Terminal Commands
Running a Gomplate template and piping the result to Claude Code to get a JSON array of tasks:
gomplate -f prompt.txt | claude -p
The full pipeline: prompt generation, AI processing, and piping the final JSON into a Bun script for processing.
gomplate -f prompt.txt | claude -p | bun run index.ts
Installing the necessary dependencies using Bun:
bun install json-schema-to-zod zod
Running the script in package.json to generate Zod types from the JSON schema:
bun run generate-types
Code Snippets
The core prompt file (prompt.txt) that uses Gomplate to include project context (repomix) and a specific JSON schema.
{{ repomix }}
<schema>
{{ file.Read "schemas/tasks.schema.json" }}
</schema>
Create a list of tasks to improve the code following the <schema>.
Output _only_ the json, nothing else. Don't even output the json markdown codefence. JUST THE JSON!!!
A simple Gomplate configuration (.gomplate.yaml) to ignore the schemas directory when gathering repository context, preventing the schema from being included twice.
plugins:
repomix:
cmd: /Users/johnlindquist/.npm-global/bin/repomix
args: ["--stdout", "--include", "**/*.json,**/*.ts", "--no-file-summary", "--ignore", "schemas"]
The final index.ts script, which reads from stdin, validates the input against an auto-generated Zod schema, and processes the now type-safe data.
import { tasksSchema, type Tasks } from "./schemas/tasks.zod";
const tasksInput = await Bun.stdin.json();
const tasks: Tasks = tasksSchema.parse(tasksInput);
console.log(tasks.map((task) => task.name));
The generated tasks.schema.json file.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Tasks",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the task"
},
"description": {
"type": "string",
"description": "A detailed description of the task"
}
},
"required": ["name", "description"],
"additionalProperties": false
}
}
The generated schemas/tasks.zod.ts file, creating a Zod schema and inferring a TypeScript type from it.
import { z } from "zod";
export const tasksSchema = z.array(z.object({ "name": z.string(), "description": z.string() }));
export type Tasks = z.infer<typeof tasksSchema>;
The scripts section of package.json with a command to regenerate the Zod types.
{
"scripts": {
"generate-types": "json-schema-to-zod -s schemas/tasks.schema.json -t schemas/tasks.zod.ts"
}
}