---
title: "Zod v4 & Gemini: Fix Structured Output with z.toJSONSchema"
slug: "zod-v4-gemini-fix-structured-output-z-tojsonschema"
published: "2025-12-24"
updated: "2026-01-03"
categories:
  - "AI"
tags:
  - "Zod v4 JSON schema"
  - "Gemini structured output"
  - "z.toJSONSchema"
  - "zod-to-json-schema"
  - "Gemini API"
  - "Google GenAI SDK"
  - "TypeScript Zod"
  - "JSON Schema conversion"
  - "AI structured output"
  - "generateContent"
  - "structured-output validation"
llm-intent: "how-to"
audience-level: "intermediate"
llm-purpose: "Zod v4 z.toJSONSchema fixes Gemini structured output failures; stop using zod-to-json-schema. Read the quick fix and update your Gemini integration today."
llm-prereqs:
  - "Zod v4"
  - "Gemini API"
  - "Google GenAI SDK"
  - "TypeScript"
  - "Node.js"
  - "pnpm"
  - "npm"
  - "Payload CMS"
---

**Summary Triples**
- (Zod v4 & Gemini: Fix Structured Output with z.toJSONSchema, expresses-intent, how-to)
- (Zod v4 & Gemini: Fix Structured Output with z.toJSONSchema, covers-topic, Zod v4 JSON schema)
- (Zod v4 & Gemini: Fix Structured Output with z.toJSONSchema, provides-guidance-for, Zod v4 z.toJSONSchema fixes Gemini structured output failures; stop using zod-to-json-schema. Read the quick fix and update your Gemini integration today.)

### {GOAL}
Zod v4 z.toJSONSchema fixes Gemini structured output failures; stop using zod-to-json-schema. Read the quick fix and update your Gemini integration today.

### {PREREQS}
- Zod v4
- Gemini API
- Google GenAI SDK
- TypeScript
- Node.js
- pnpm
- npm
- Payload CMS

### {STEPS}
1. Upgrade project to Zod v4
2. Remove zod-to-json-schema dependency
3. Convert schema with z.toJSONSchema
4. Handle and validate AI responses
5. Integrate into AI job handlers

<!-- llm:goal="Zod v4 z.toJSONSchema fixes Gemini structured output failures; stop using zod-to-json-schema. Read the quick fix and update your Gemini integration today." -->
<!-- llm:prereq="Zod v4" -->
<!-- llm:prereq="Gemini API" -->
<!-- llm:prereq="Google GenAI SDK" -->
<!-- llm:prereq="TypeScript" -->
<!-- llm:prereq="Node.js" -->
<!-- llm:prereq="pnpm" -->
<!-- llm:prereq="npm" -->
<!-- llm:prereq="Payload CMS" -->

# Zod v4 & Gemini: Fix Structured Output with z.toJSONSchema
> Zod v4 z.toJSONSchema fixes Gemini structured output failures; stop using zod-to-json-schema. Read the quick fix and update your Gemini integration today.
Matija Žiberna · 2025-12-24

I was building an AI content generation system with Gemini when I followed Google's official documentation on structured output, only to discover the recommended library wasn't compatible with Zod v4. After debugging incomplete JSON schemas for hours, I found out Zod v4 introduced native JSON schema conversion that makes the third-party library obsolete. Here's the exact implementation that actually works.

## The Problem: Google's Docs Are Pointing You to Incompatible Code

Google's [Gemini API documentation for structured output](https://ai.google.dev/gemini-api/docs/structured-output?example=recursive) recommends using `zod-to-json-schema`, a third-party library:

```typescript
import { zodToJsonSchema } from "zod-to-json-schema";
```

The problem is that `zod-to-json-schema` v3.25.1 (the latest version) was built for Zod v3. When you use it with Zod v4, the schema conversion silently fails. Instead of returning your full schema with `properties`, `type`, and `required` fields, it returns an incomplete object:

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#"
}
```

This incomplete schema tells Gemini to ignore your structured output constraints entirely. The model then returns whatever field names it wants (like `seo_title` instead of `title`), completely defeating the purpose of schema enforcement.

The root cause: **Zod v4 introduces native JSON schema conversion**, making the external library redundant and breaking compatibility.

## The Solution: Use Zod v4's Native `z.toJSONSchema()`

Zod v4 ships with a built-in `z.toJSONSchema()` method that works properly out of the box. No external dependencies needed.

### Step 1: Upgrade to Zod v4

First, ensure you're on Zod v4 (currently v4.2.1 or later):

```bash
pnpm add zod@^4
```

Or if you're using npm:

```bash
npm install zod@^4
```

### Step 2: Remove the Incompatible Library

If you have `zod-to-json-schema` in your dependencies, remove it:

```bash
pnpm remove zod-to-json-schema
```

You no longer need it. Zod v4 has everything built-in.

### Step 3: Update Your Gemini Integration

Here's the corrected implementation for your Gemini SDK integration:

```typescript
// File: src/lib/gemini.ts
import { GenerateContentConfig, GenerateContentResponse, GoogleGenAI } from '@google/genai';
import { z } from 'zod';

const GEMINI_API_KEY = process.env.GEMINI_API_KEY;

if (!GEMINI_API_KEY) {
    console.warn('GEMINI_API_KEY is not defined in environment variables');
}

export const genAI = new GoogleGenAI({ apiKey: GEMINI_API_KEY || '' });
export const DEFAULT_MODEL = 'gemini-2.5-flash';

function extractTextFromResponse(result: GenerateContentResponse): string {
    const candidate = result.candidates?.[0];
    if (!candidate) {
        console.error('[Gemini Error] No candidates returned');
        return '';
    }

    const content = candidate.content;
    if (!content || !content.parts || content.parts.length === 0) {
        console.error('[Gemini Error] No content parts returned');
        return '';
    }

    const textPart = content.parts.find(p => 'text' in p);
    const text = textPart ? textPart.text : '';

    if (!text) {
        console.error('[Gemini Error] No text found in content parts');
        return '';
    }

    return text;
}

export async function generateAIContent<T extends z.ZodTypeAny | undefined = undefined>(
    input: string | Array<string | { inlineData: { mimeType: string; data: string } }>,
    modelName: string = DEFAULT_MODEL,
    schema?: T
): Promise<T extends z.ZodTypeAny ? z.infer<T> : string> {
    try {
        const contents = typeof input === 'string' ? input : input;

        const config: GenerateContentConfig = {};
        if (schema) {
            // Use Zod v4's native z.toJSONSchema() method
            const jsonSchema = z.toJSONSchema(schema as any);

            console.log('[Gemini] Config Schema:', JSON.stringify(jsonSchema, null, 2));
            (config as any).responseJsonSchema = jsonSchema;
            config.responseMimeType = 'application/json';
        }

        const result = await genAI.models.generateContent({
            model: modelName,
            contents,
            config
        }) as GenerateContentResponse;

        let responseStr: string | undefined;
        if (result.text) {
            responseStr = result.text;
        }

        if (!responseStr) {
            responseStr = extractTextFromResponse(result);
        }

        if (!responseStr) {
            throw new Error('No response from AI');
        }

        if (schema) {
            // Clean up markdown code blocks if present
            const cleanJson = responseStr.replace(/```json\n?|\n?```/g, '').trim();
            try {
                const parsed = JSON.parse(cleanJson);
                return schema.parse(parsed) as T extends z.ZodTypeAny ? z.infer<T> : string;
            } catch (e) {
                console.error('[Gemini] Failed to parse or validate JSON response:', cleanJson);
                throw new Error(`Invalid AI response: ${e instanceof Error ? e.message : String(e)}`);
            }
        }

        console.log('[Gemini] Response:', responseStr);
        return responseStr as unknown as T extends z.ZodTypeAny ? z.infer<T> : string;
    } catch (error) {
        console.error('Error generating AI content:', error);
        throw error;
    }
}
```

The critical change is on line 48: instead of importing and using `zodToJsonSchema`, we call `z.toJSONSchema()` directly on the Zod schema. This is a native Zod v4 method that generates complete, valid JSON schemas.

### Step 4: Use It in Your AI Jobs

Now when you define a Zod schema in your AI job handlers, the structured output will work correctly:

```typescript
// File: src/payload/jobs/ai/seo.ts
import type { TaskHandler } from 'payload';
import { z } from 'zod';
import { generateAIContent } from '@/lib/gemini';

export const aiGenerateSeoHandler: TaskHandler<'ai-generate-seo'> = async ({ input, req }) => {
    const { docId, collection, tenantId } = input;

    // ... document fetching and verification ...

    const { title, contentText } = await getDocContent(req, collection as CollectionName, docId);

    const prompt = `Generate a concise SEO title (50-60 chars max) and meta description (100-150 chars max) for this article.

Title: ${title}
Content: ${contentText.substring(0, 5000)}

Requirements:
- SEO title: 50-60 characters (include spaces)
- Meta description: 100-150 characters (include spaces)
- Be concise and keyword-focused
- Include main topic from article`;

    // Define your schema with clear field names
    const seoSchema = z.object({
        title: z.string().describe("The SEO title (50-60 characters)"),
        description: z.string().describe("The meta description (100-150 characters)"),
    });

    // Pass the schema to generateAIContent
    const seo = await generateAIContent(prompt, undefined, seoSchema);

    // seo is now properly typed and validated
    await req.payload.update({
        collection: collection as CollectionName,
        id: docId,
        draft: true,
        data: {
            meta: {
                title: seo.title,
                description: seo.description,
            }
        },
        context: {
            disableRevalidation: true,
        },
    });

    return { output: { message: 'SEO regenerated successfully' } };
};
```

When you pass the schema to `generateAIContent`, the function now converts it using `z.toJSONSchema()`, which produces the complete schema:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "title": {
      "description": "The SEO title (50-60 characters)",
      "type": "string"
    },
    "description": {
      "description": "The meta description (100-150 characters)",
      "type": "string"
    }
  },
  "required": [
    "title",
    "description"
  ],
  "additionalProperties": false
}
```

Gemini now receives the full schema and properly enforces it. The model returns `title` and `description` fields exactly as specified, not some variation like `seo_title`.

## Why This Matters

The difference between these two approaches is the difference between structured output that works and structured output that silently fails. Using Google's recommended library with Zod v4 leaves you debugging schema validation errors that don't actually exist—the problem is upstream in the schema conversion itself.

By using Zod v4's native method, you:
- Eliminate the external dependency
- Get proper schema conversion out of the box
- Have full type safety with TypeScript
- Follow the actual Zod v4 design (as documented at [zod.dev/json-schema](https://zod.dev/json-schema))
- Avoid the compatibility nightmare that caught me and many other developers

## Closing Note

Google's documentation should be updated to recommend `z.toJSONSchema()` instead of the third-party library, or at minimum add a compatibility note. If you encounter this issue, report it to [Google's issue tracker](https://github.com/google-ai-sdk/google-ai-javascript-sdk) so they update the official examples.

Let me know in the comments if you hit any issues implementing this, and subscribe for more practical development guides.

Thanks, Matija

---

## Sources

- [Zod v4 JSON Schema Documentation](https://zod.dev/json-schema)
- [Gemini API Documentation - Structured Output](https://ai.google.dev/gemini-api/docs/structured-output)
- [Google GenAI JavaScript SDK](https://github.com/google-ai-sdk/google-ai-javascript-sdk)
- [Zod GitHub Repository](https://github.com/colinhacks/zod)