GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-02Intermediate

Zod × Gemini API: Type-Safe Structured Output Validation in TypeScript

Pattern for validating Gemini API structured output with Zod schemas. Covers why type casting is unsafe, JSON Schema conversion, and retry design when validation fails — with working TypeScript code.

gemini-api277zod3typescript15structured-output22validation3

When you wire up Gemini API with responseMimeType: "application/json" and a responseSchema, are you using JSON.parse(response.text) as MyType on the TypeScript side and calling it a day? I did, for a while, before noticing in production that LLM output mostly conforms to the schema but occasionally drifts — wrong field order, an unexpected null, an integer arriving as a string. Code that relies on as happily lets all of those slip through.

This article walks through a pattern for validating Gemini responses at runtime with Zod. By treating the Zod schema as the single source of truth, both the JSON Schema sent to Gemini and the TypeScript types in your code can be derived from one definition.

Why as casting falls short

TypeScript type casts are a compile-time-only check. The moment you write as MyType, the compiler trusts you. It never inspects the actual value.

// ❌ Unsafe pattern
const result = await ai.models.generateContent({
  model: "gemini-2.5-flash",
  contents: prompt,
  config: { responseMimeType: "application/json", responseSchema: schema },
});
const data = JSON.parse(result.text) as Article;
console.log(data.title.toUpperCase()); // throws if data.title is undefined

Gemini's structured output is accurate — it returns the requested shape well over 90% of the time. But the remaining few percent matter. From a user's perspective, an app that crashes occasionally is an unstable app. In production you need to detect those drifts rather than swallow them.

Zod is a widely adopted runtime validation library in the TypeScript ecosystem. You can derive types from a schema definition, and parse() either returns a typed value or throws a detailed error. It pairs well with Gemini because zod-to-json-schema can convert a Zod schema into the JSON Schema format Gemini expects.

The basic flow: passing a Zod schema to Gemini

Install the dependencies first.

npm install @google/genai zod zod-to-json-schema

Then define a Zod schema for the response. We'll use review extraction as the running example.

// schema.ts
import { z } from "zod";
 
export const ReviewSchema = z.object({
  productName: z.string().min(1).describe("Product the review is about"),
  rating: z.number().int().min(1).max(5).describe("1-5 satisfaction score"),
  sentiment: z.enum(["positive", "neutral", "negative"]),
  pros: z.array(z.string()).max(5),
  cons: z.array(z.string()).max(5),
});
 
export type Review = z.infer<typeof ReviewSchema>;

z.infer extracts the TypeScript type, so everything else in your codebase derives from this single file. Change the schema and the types follow automatically.

When passing this to Gemini, convert it with zod-to-json-schema.

// gemini.ts
import { GoogleGenAI } from "@google/genai";
import { zodToJsonSchema } from "zod-to-json-schema";
import { ReviewSchema, type Review } from "./schema";
 
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
 
export async function extractReview(rawText: string): Promise<Review> {
  const jsonSchema = zodToJsonSchema(ReviewSchema, {
    target: "openApi3", // Gemini accepts OpenAPI 3-compatible schemas
  });
 
  const response = await ai.models.generateContent({
    model: "gemini-2.5-flash",
    contents: `Extract structured fields from this review:\n\n${rawText}`,
    config: {
      responseMimeType: "application/json",
      responseSchema: jsonSchema,
    },
  });
 
  return ReviewSchema.parse(JSON.parse(response.text ?? "{}"));
}

Two things matter here. First, responseSchema tells Gemini what shape to return. Second, the response is re-validated with ReviewSchema.parse() on your side. This double check is the heart of the Zod integration. Gemini's structured output is strong, but you don't want to depend on it alone — validating again locally lets you catch unexpected responses safely.

Retry design when validation fails

When parse() throws, dropping the request is rarely the right move. Feeding the validation error back to Gemini and retrying is a more practical pattern.

// retry.ts
import { z } from "zod";
 
export async function extractWithRetry<T>(
  schema: z.ZodSchema<T>,
  generate: (errorContext?: string) => Promise<string>,
  maxAttempts = 3,
): Promise<T> {
  let lastError: string | undefined;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const text = await generate(lastError);
    try {
      return schema.parse(JSON.parse(text));
    } catch (e) {
      if (e instanceof z.ZodError) {
        lastError = `Previous response failed validation: ${e.issues
          .map((i) => `${i.path.join(".")}: ${i.message}`)
          .join("; ")}. Please follow the schema exactly.`;
        if (attempt === maxAttempts) throw e;
      } else {
        throw e;
      }
    }
  }
  throw new Error("unreachable");
}

With this helper, the rare drift becomes recoverable: include the error message in the next prompt and Gemini will usually fix the shape on the second attempt. In practice three attempts is a generous ceiling.

Three pitfalls worth knowing

A few things tripped me up while building this pattern, and they're worth flagging.

1. z.union and z.discriminatedUnion

Gemini supports oneOf only partially. A plain z.union produces JSON Schema with oneOf, which sometimes confuses the model. z.discriminatedUnion is safer when it works; if it still misbehaves, fall back to expressing the variant via z.enum and a regular object.

2. Don't skimp on .describe()

Zod's .describe() text becomes the JSON Schema description, which is fed to Gemini. This directly affects accuracy. "rating: 1-5 score" is fine. "rating: user satisfaction as an integer 1-5, where 1 is very poor and 5 is excellent" yields noticeably more consistent output.

3. Avoid z.date()

JSON has no native date type, so Gemini returns strings. Define the schema as z.string().datetime() and add .transform((v) => new Date(v)) if you need a Date object downstream. Passing z.date() to zodToJsonSchema will produce a schema that Gemini cannot satisfy.

Sharing the schema between client and server

If you're on a fullstack framework like Next.js, place the Zod schema in something like lib/schemas.ts and use it from both the server-side Gemini call and the client-side form. With one source of truth you avoid the classic bug where someone adds a field on one side but forgets the other.

A practical setup: the API route receives Gemini's response, validates with Zod, and returns JSON to the client. The client revalidates with the same schema before storing the result in React state. Since the type comes from z.infer, frontend and backend stay perfectly aligned.

For going deeper on response patterns, Production response schema validation for Gemini API and Designing type-safe AI applications in TypeScript cover related ground. If you'd rather implement the same pattern in Python, Gemini API × Pydantic for type-safe structured output is the companion piece.

Testing the integration without burning quota

One side benefit of having a Zod schema is that your tests can validate fixture data without ever hitting Gemini. Write a JSON file that represents a typical (or edge-case) response, load it in a test, and run ReviewSchema.parse() against it. Any time you adjust the schema, the test suite tells you whether your fixtures are still valid — without spending a single API call.

// schema.test.ts
import { describe, it, expect } from "vitest";
import { ReviewSchema } from "./schema";
 
describe("ReviewSchema", () => {
  it("accepts a well-formed response", () => {
    const fixture = {
      productName: "Wireless Earbuds X",
      rating: 4,
      sentiment: "positive",
      pros: ["good battery", "comfortable fit"],
      cons: ["app is buggy"],
    };
    expect(() => ReviewSchema.parse(fixture)).not.toThrow();
  });
 
  it("rejects an invalid sentiment value", () => {
    const fixture = { productName: "X", rating: 3, sentiment: "okay", pros: [], cons: [] };
    expect(() => ReviewSchema.parse(fixture)).toThrow();
  });
});

The combination of a strict schema and a few representative fixtures catches most regressions before they reach the LLM call.

When to reach for safeParse instead of parse

By default parse() throws on invalid data, which is fine inside a service that has retry wrapping. In code paths where you want to inspect the result without the cost of try/catch, safeParse() returns a discriminated union you can pattern match on:

const result = ReviewSchema.safeParse(JSON.parse(text));
if (!result.success) {
  logger.warn({ issues: result.error.issues }, "validation failed");
  return fallbackReview;
}
return result.data;

This is especially handy in serverless handlers where you'd rather degrade gracefully than throw a 500 to the user. Choose based on the surrounding context: parse for hot paths protected by a retry loop, safeParse when the caller wants to branch on success.

Start with a small schema

The nice thing about adopting Zod here is that you can start tiny and grow. Begin with a two- or three-field object, then add nesting, arrays, and enums as you get comfortable. Pick one Gemini structured-output endpoint in your current app and wrap a single parse() around it today. That alone tends to surface drifts you never noticed before.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-06-25
Gemini API × TypeScript Type-Safe AI Application Architecture — Integrating Zod Schemas, Structured Output, and Streaming
Type-safe AI applications with the Gemini API and TypeScript: Zod validation, Structured Output, streaming pipelines, and error handling that holds up in production.
API / SDK2026-06-25
When Gemini's Structured Output Quietly Drifts From Your Schema — Field Notes on Measuring Validation and Retries
Even with response_schema set, Gemini's structured output occasionally drifts in production. Stop swallowing failures, measure them, split causes by finish_reason, and feed errors back for a corrected retry. Field notes from stabilizing a validation pipeline.
API / SDK2026-05-23
Why Your Gemini API Structured Output Keeps Failing Validation — and How to Stabilize It
A field guide to the three layers where Gemini API structured output breaks — server-side schema rejection, silent empty responses, and client-side parsing — with practical fixes from an indie developer's production AdMob reporting pipeline.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →