TypeScript AI Structured Outputs

A practical reference for generating guaranteed type-safe JSON data from AI models using Zod schemas and structured output modes.

TL;DR

  1. Define expected data structures using z.object() and Zod primitives.
  2. Derive static TypeScript types directly from runtime schemas with z.infer<typeof Schema>.
  3. Enforce JSON output schemas with native provider response_format flags.

Schema Definition with Zod

    z.object() definition

    Declare structured payload shape using Zod schema builders.

    import { z } from 'zod';
    const UserSchema = z.object({
      name: z.string().describe('Full user name'),
      age: z.number().int().positive(),
      roles: z.array(z.enum(['admin', 'editor'])),
    });
    z.infer<typeof Schema>

    Derive static TypeScript types directly from runtime schemas.

    type User = z.infer<typeof UserSchema>;
    // Inferred type:
    // {
    //   name: string;
    //   age: number;
    //   roles: ('admin' | 'editor')[];
    // }
    Optional and nullable fields

    Model optional properties and nullable attributes cleanly.

    const PostSchema = z.object({
      title: z.string(),
      summary: z.string().optional(),
      publishedAt: z.string().datetime().nullable(),
    });

Native Provider Configuration

    OpenAI json_schema mode

    Pass a Zod schema as response_format.

    import { zodResponseFormat } from 'openai/helpers/zod';
    await openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: 'Extract' }],
      response_format: zodResponseFormat(User, 'u'),
    });
    Gemini responseSchema

    Set a JSON schema in config.

    await ai.models.generateContent({
      model: 'gemini-2.0-flash',
      contents: 'Extract user',
      config: {
        responseMimeType: 'application/json',
        responseSchema: { type: 'OBJECT' },
      },
    });
    Anthropic tool constraint

    Force Claude to call one tool.

    await anthropic.messages.create({
      model, max_tokens: 500,
      tools: [{ name: 'save', input_schema: {} }],
      tool_choice: { type: 'tool', name: 'save' },
      messages: [{ role: 'user', content: 'Hi' }],
    });

Safe Parsing and Type Narrowing

    Schema.safeParse()

    Validate without throwing.

    const p = UserSchema.safeParse(JSON.parse(raw));
    if (!p.success) console.error(p.error.issues);
    else console.log(p.data.name);
    Format Zod errors

    Flatten issues into readable strings.

    function fmt(err: z.ZodError): string {
      return err.issues
        .map(i => `${i.path.join('.')}: ${i.message}`)
        .join(', ');
    }
    Type-safe wrapper function

    Generic helper that returns a typed object.

    async function extract<T>(
      schema: z.ZodType<T>, prompt: string
    ): Promise<T> {
      const raw = await fetchCompletion(prompt);
      return schema.parse(JSON.parse(raw));
    }

Automated Schema Self-Repair

    Self-repair prompt loop

    Feed the error back and ask for a fix.

    async function repair<T>(
      schema: z.ZodType<T>, bad: string, err: string
    ): Promise<T> {
      const res = await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [{ role: 'user', content: `${bad}: ${err}` }],
      });
      return schema.parse(JSON.parse(res.choices[0].message.content!));
    }
    Retry with backoff loop

    Up to three attempts before giving up.

    for (let i = 0; i < 3; i++) {
      try { return await extract(UserSchema, prompt); }
      catch {}
    }
    throw new Error('Retries failed');
    Schema default values

    Fallbacks for non-critical fields.

    const Settings = z.object({
      theme: z.enum(['light', 'dark']).default('light'),
      retries: z.number().default(3),
    });

Tips

  1. Use z.infer<typeof Schema> to keep your compile-time TypeScript interfaces perfectly synchronized with your runtime model validation rules.
  2. Attach .describe() helper annotations to your Zod schema keys to provide contextual instructions that guide the model during extraction.

Warnings

  1. Never assume an AI model response is valid JSON without wrapping JSON.parse and .safeParse() inside defensive error handlers.
  2. Avoid overly deep nested object schemas because recursive validation constraints can cause models to exhaust their max_tokens budget.

In Practice

FAQ