Building a Structured Output Pipeline with Gemini Flash and Zod
Structured output is the technique that turns unreliable LLM prose into validated, type-safe JSON. This guide walks through building a production-ready pipeline using Gemini Flash (Google's cost-efficient model with native JSON output) and Zod for runtime validation in Node.js. By the end, you'll have a reusable pattern for any content generation use case.
Set up your project and install dependencies
Create a new Node.js project and install the Google Generative AI SDK and Zod. We'll use the official @google/generative-ai package which has native structured output support.
mkdir structured-output-demo && cd structured-output-demo
npm init -y
npm install @google/generative-ai zod
npm install -D typescript @types/node tsx
npx tsc --init --strict⚠ Common Pitfalls
- •Use the @google/generative-ai package (not @google/genai which is a different SDK)
- •TypeScript strict mode is recommended — Zod inferred types work best with strict null checks
Define your output schema with Zod
Before writing any generation code, define exactly what you want the LLM to produce. A Zod schema serves two purposes: it generates a JSON Schema for the API (constraining the model) and validates the output at runtime. Here's a schema for a resource article section.
import { z } from 'zod';
export const ArticleSectionSchema = z.object({
heading: z.string().describe('Section heading, concise and keyword-rich'),
items: z.array(
z.object({
title: z.string().describe('Item title, under 80 characters'),
description: z.string().describe('2-3 sentence description of the item'),
difficulty: z.enum(['beginner', 'intermediate', 'advanced'])
.describe('Technical difficulty level for the reader'),
})
).min(5).max(10).describe('List of actionable items in this section'),
});
export type ArticleSection = z.infer<typeof ArticleSectionSchema>;⚠ Common Pitfalls
- •Use .describe() on fields — these descriptions become part of the JSON Schema sent to the model and improve output quality significantly
- •Keep schemas flat where possible — deeply nested schemas increase token usage and hallucination risk
Convert your Zod schema to JSON Schema for the API
Gemini's responseSchema parameter accepts JSON Schema format. You can use zod-to-json-schema to convert your Zod schema automatically, avoiding duplication.
import { zodToJsonSchema } from 'zod-to-json-schema';
import type { ZodType } from 'zod';
export function toJsonSchema(schema: ZodType) {
const jsonSchema = zodToJsonSchema(schema, { target: 'openApi3' });
// Remove unsupported fields that Gemini rejects
const { $schema, ...cleanSchema } = jsonSchema as Record<string, unknown>;
return cleanSchema;
}⚠ Common Pitfalls
- •Install zod-to-json-schema: npm install zod-to-json-schema
- •The target: 'openApi3' option produces cleaner output than the default JSON Schema draft
- •Gemini rejects some JSON Schema keywords — strip $schema and $defs if they cause errors
Initialize the Gemini client and configure structured output
The key configuration is setting responseMimeType to 'application/json' and providing the responseSchema. This constrains the model to produce output matching your schema rather than freeform text.
import { GoogleGenerativeAI } from '@google/generative-ai';
import { ArticleSectionSchema } from './schemas/article-section.js';
import { toJsonSchema } from './utils/schema-to-json.js';
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-flash',
generationConfig: {
responseMimeType: 'application/json',
responseSchema: toJsonSchema(ArticleSectionSchema) as object,
temperature: 0.7,
},
});⚠ Common Pitfalls
- •Never hardcode your API key — use process.env.GEMINI_API_KEY
- •gemini-1.5-flash is more cost-effective than gemini-pro for structured output tasks
- •Lower temperature (0.3-0.7) produces more consistent structured output
Write the generation function with Zod validation
The generation function calls the model and immediately validates the response with your Zod schema. If validation fails, you can retry with a corrective prompt or log the failure. This is your validation gate.
import { ArticleSectionSchema, type ArticleSection } from './schemas/article-section.js';
export async function generateSection(
niche: string,
heading: string
): Promise<ArticleSection> {
const prompt = `
Generate a resource article section for the niche: "${niche}".
Section heading: "${heading}"
Produce exactly 7 actionable items. Each item needs a title, a 2-3 sentence description,
and a difficulty level. Focus on practical, specific advice — not generic tips.
`.trim();
const result = await model.generateContent(prompt);
const raw = result.response.text();
// Parse and validate — will throw if schema doesn't match
const parsed = JSON.parse(raw);
return ArticleSectionSchema.parse(parsed);
}⚠ Common Pitfalls
- •Always JSON.parse() before Zod validation — the response is a string even with JSON mode enabled
- •Wrap in a try/catch in production and implement retry logic for validation failures
- •Log the raw response on validation failure — it helps debug schema mismatches
Add retry logic for production robustness
Even with structured output, models occasionally produce invalid JSON or schema-mismatched content (especially for complex schemas). A simple retry wrapper with exponential backoff handles transient failures.
export async function withRetry<T>(
fn: () => Promise<T>,
maxAttempts = 3,
delayMs = 1000
): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
console.warn(`Attempt ${attempt}/${maxAttempts} failed:`, error);
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delayMs * attempt));
}
}
}
throw lastError;
}⚠ Common Pitfalls
- •Don't retry indefinitely — cap at 3-5 attempts to avoid runaway API costs
- •Log all failures even when retry succeeds — failure patterns reveal schema or prompt issues
- •For batch generation, track failure rate; above 5% usually means a schema or prompt problem
What you built
You now have a robust structured output pipeline: a Zod schema that defines your data contract, a JSON Schema conversion for the API, a generation function with runtime validation, and retry logic for production reliability. This pattern scales to any content type — change the schema and prompt, and the pipeline adapts. The validation gate is the key insight: no invalid content reaches your application.