Module 2 (Gemini) - Lesson 2f: Structured Output with Gemini

Request JSON responses with responseMimeType and validate with Zod.

Published: 1/15/2026

Lesson 2f: Structured Output with Google Gemini

Learn how to get reliable, validated JSON responses from Gemini. Unlike Anthropic (prompt-based) and similar to OpenAI, Gemini has native JSON output support.

Key Differences from OpenAI and Anthropic

OpenAI: Native structured output with response_format

response_format: { type: "json_object" }

Anthropic: Prompt-based JSON (no native support)

// Must request JSON in prompt and parse manually
content: "Return only valid JSON..."

Gemini: Native JSON with responseMimeType and optional schema

config: {
  responseMimeType: "application/json",
  responseJsonSchema: { ... }
}

Code Example

Create src/gemini/structured-output-prompt.ts:

import { GoogleGenAI, ApiError } from "@google/genai";
import dotenv from "dotenv";
import { z } from "zod";

// Load environment variables
dotenv.config();

// Define Zod schema for validation
const ChristmasMarkets = z.array(
  z.object({
    country: z.string().describe("Country where the market is located"),
    city: z.string().describe("City where the market is located"),
    name: z.string().describe("Name of the Christmas market"),
    description: z.string().describe("Brief description of the market"),
    address: z.string().describe("Address of the market"),
  })
);

// Create Gemini client
const gemini = new GoogleGenAI({});

async function structuredOutputPrompt(country: string): Promise<void> {
  try {
    console.log("Testing Gemini connection...");

    const response = await gemini.models.generateContent({
      model: "gemini-3-flash-preview",
      contents: {
        role: "user",
        parts: [
          {
            text: `Provide a list of famous Christmas markets in ${country} with a brief description and address for each market.`,
          },
        ],
      },
      config: {
        systemInstruction:
          "You are a helpful travel assistant. Extract Christmas market info in JSON.",
        responseMimeType: "application/json",
        responseJsonSchema: {
          type: "array",
          items: {
            type: "object",
            properties: {
              country: { type: "string" },
              city: { type: "string" },
              name: { type: "string" },
              description: { type: "string" },
              address: { type: "string" },
            },
            required: ["country", "city", "name", "description", "address"],
          },
        },
      },
    });

    console.log("Structured Output Prompt Success!");
    console.log("Tokens used:");
    console.dir(response.usageMetadata, { depth: null });

    if (!response.text) {
      throw new Error("No content in response");
    }

    // Parse JSON and validate with Zod
    const jsonData = JSON.parse(response.text);
    const markets = ChristmasMarkets.parse(jsonData);

    console.log("AI Response (validated):");
    console.log(JSON.stringify(markets, null, 2));
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.log("Validation Error:", error.errors);
    } else if (error instanceof ApiError) {
      console.log("API Error:", error.status, error.message);
    } else if (error instanceof Error) {
      console.log("Error:", error.message);
    }
  }
}

// Run with command line argument or default
const country = process.argv[2] ?? "Austria";
structuredOutputPrompt(country);

Run It

pnpm tsx src/gemini/structured-output-prompt.ts Austria
pnpm tsx src/gemini/structured-output-prompt.ts Germany

Understanding the Approach

1. Set Response MIME Type

Tell Gemini to return JSON:

config: {
  responseMimeType: "application/json"
}

2. Define JSON Schema (Optional but Recommended)

Provide a schema to guide the structure:

config: {
  responseMimeType: "application/json",
  responseJsonSchema: {
    type: "object",
    properties: {
      name: { type: "string" },
      age: { type: "number" },
      email: { type: "string" }
    },
    required: ["name", "age", "email"]
  }
}

3. Validate with Zod

Always validate the response for type safety:

import { z } from "zod";

const UserSchema = z.object({
  name: z.string(),
  age: z.number().int().positive(),
  email: z.string().email()
});

const data = JSON.parse(response.text);
const validated = UserSchema.parse(data);

// TypeScript knows the exact shape!
console.log(validated.name);  // Type-safe!

JSON Schema Syntax

Basic Types

responseJsonSchema: {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number" },
    active: { type: "boolean" },
    tags: {
      type: "array",
      items: { type: "string" }
    }
  },
  required: ["name", "age"]
}

Nested Objects

responseJsonSchema: {
  type: "object",
  properties: {
    user: {
      type: "object",
      properties: {
        name: { type: "string" },
        address: {
          type: "object",
          properties: {
            street: { type: "string" },
            city: { type: "string" }
          }
        }
      }
    }
  }
}

Arrays of Objects

responseJsonSchema: {
  type: "array",
  items: {
    type: "object",
    properties: {
      id: { type: "number" },
      name: { type: "string" }
    },
    required: ["id", "name"]
  }
}

Enums

responseJsonSchema: {
  type: "object",
  properties: {
    status: {
      type: "string",
      enum: ["pending", "active", "completed"]
    },
    priority: {
      type: "string",
      enum: ["low", "medium", "high"]
    }
  }
}

Practical Examples

1. Data Extraction

const InvoiceSchema = z.object({
  invoiceNumber: z.string(),
  date: z.string(),
  total: z.number(),
  items: z.array(z.object({
    description: z.string(),
    quantity: z.number(),
    price: z.number()
  }))
});

const response = await gemini.models.generateContent({
  model: "gemini-3-flash-preview",
  contents: `Extract data from this invoice:

Invoice #INV-2024-001
Date: 2024-01-15
Items:
- MacBook Pro (1x) $2,499
- Magic Mouse (2x) $79 each
Total: $2,657`,
  config: {
    systemInstruction: "Extract invoice data as JSON.",
    responseMimeType: "application/json",
    responseJsonSchema: {
      type: "object",
      properties: {
        invoiceNumber: { type: "string" },
        date: { type: "string" },
        total: { type: "number" },
        items: {
          type: "array",
          items: {
            type: "object",
            properties: {
              description: { type: "string" },
              quantity: { type: "number" },
              price: { type: "number" }
            }
          }
        }
      }
    }
  }
});

const invoice = InvoiceSchema.parse(JSON.parse(response.text));

2. Sentiment Analysis

const SentimentSchema = z.object({
  sentiment: z.enum(["positive", "negative", "neutral"]),
  confidence: z.number().min(0).max(1),
  keywords: z.array(z.string()),
  summary: z.string()
});

const response = await gemini.models.generateContent({
  model: "gemini-3-flash-preview",
  contents: `Analyze the sentiment of this review:

"This product exceeded my expectations! The quality is amazing and
customer service was incredibly helpful. Highly recommend!"`,
  config: {
    responseMimeType: "application/json",
    responseJsonSchema: {
      type: "object",
      properties: {
        sentiment: { type: "string", enum: ["positive", "negative", "neutral"] },
        confidence: { type: "number" },
        keywords: { type: "array", items: { type: "string" } },
        summary: { type: "string" }
      },
      required: ["sentiment", "confidence", "keywords", "summary"]
    }
  }
});

const result = SentimentSchema.parse(JSON.parse(response.text));
console.log(`Sentiment: ${result.sentiment} (${result.confidence * 100}%)`);

3. Recipe Extraction

const RecipeSchema = z.object({
  name: z.string(),
  servings: z.number(),
  prepTimeMinutes: z.number(),
  difficulty: z.enum(["easy", "medium", "hard"]),
  ingredients: z.array(z.object({
    item: z.string(),
    amount: z.string(),
    unit: z.string()
  })),
  steps: z.array(z.string()),
  tags: z.array(z.string())
});

const response = await gemini.models.generateContent({
  model: "gemini-3-flash-preview",
  contents: `Extract this recipe as JSON:

Chocolate Chip Cookies (Makes 24 cookies, 30 min prep, Easy)

Ingredients:
- 2 cups all-purpose flour
- 1 tsp baking soda
- 1 cup butter, softened
- 3/4 cup sugar
- 2 eggs
- 2 cups chocolate chips

Steps:
1. Preheat oven to 375°F
2. Mix dry ingredients
3. Cream butter and sugar
4. Add eggs and mix well
5. Combine wet and dry ingredients
6. Fold in chocolate chips
7. Drop spoonfuls on baking sheet
8. Bake for 10-12 minutes

Tags: dessert, baking, cookies, chocolate`,
  config: {
    responseMimeType: "application/json",
    responseJsonSchema: {
      type: "object",
      properties: {
        name: { type: "string" },
        servings: { type: "number" },
        prepTimeMinutes: { type: "number" },
        difficulty: { type: "string", enum: ["easy", "medium", "hard"] },
        ingredients: {
          type: "array",
          items: {
            type: "object",
            properties: {
              item: { type: "string" },
              amount: { type: "string" },
              unit: { type: "string" }
            }
          }
        },
        steps: { type: "array", items: { type: "string" } },
        tags: { type: "array", items: { type: "string" } }
      }
    }
  }
});

const recipe = RecipeSchema.parse(JSON.parse(response.text));

Error Handling

Always handle potential parsing and validation errors:

async function getStructuredOutput<T>(
  schema: z.ZodSchema<T>,
  jsonSchema: object,
  prompt: string
): Promise<T> {
  try {
    const response = await gemini.models.generateContent({
      model: "gemini-3-flash-preview",
      contents: prompt,
      config: {
        responseMimeType: "application/json",
        responseJsonSchema: jsonSchema
      }
    });

    if (!response.text) {
      throw new Error("No content in response");
    }

    const jsonData = JSON.parse(response.text);
    return schema.parse(jsonData);

  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error("Validation error:", error.errors);
      throw new Error(`Invalid data structure: ${error.message}`);
    } else if (error instanceof SyntaxError) {
      console.error("JSON parsing error");
      throw new Error("Invalid JSON in response");
    } else if (error instanceof ApiError) {
      console.error("API error:", error.status, error.message);
      throw error;
    } else {
      throw error;
    }
  }
}

// Usage
const markets = await getStructuredOutput(
  ChristmasMarkets,
  marketsJsonSchema,
  "List Christmas markets in Germany"
);

Provider Comparison

OpenAI Approach

const response = await openai.chat.completions.create({
  model: "gpt-4o",
  response_format: { type: "json_object" },
  messages: [
    { role: "system", content: "Return JSON" },
    { role: "user", content: "Extract data..." }
  ]
});

const data = JSON.parse(response.choices[0].message.content);

Anthropic Approach

// No native JSON mode - must use prompt
const response = await anthropic.messages.create({
  model: "claude-haiku-4-5",
  max_tokens: 1000,
  system: "Return only valid JSON, no other text.",
  messages: [{
    role: "user",
    content: `Extract data as JSON matching this schema:
    ${JSON.stringify(schema)}

    Data: ...`
  }]
});

// Must strip markdown if present
let text = response.content[0].text;
if (text.startsWith("```json")) {
  text = text.replace(/^```json\n/, "").replace(/\n```$/, "");
}
const data = JSON.parse(text);

Gemini Approach

const response = await gemini.models.generateContent({
  model: "gemini-3-flash-preview",
  contents: "Extract data...",
  config: {
    responseMimeType: "application/json",
    responseJsonSchema: {
      type: "object",
      properties: { ... }
    }
  }
});

const data = JSON.parse(response.text);

Comparison Table

FeatureOpenAIAnthropicGemini
Native JSON modeYesNoYes
Schema supportLimitedNo (prompt only)Yes
ReliabilityHighMediumHigh
Config locationresponse_formatPromptconfig.responseMimeType
Needs parsingYesYes + cleanupYes

Best Practices

1. Always Use Zod Validation

// Even with schema, always validate
const data = JSON.parse(response.text);
const validated = schema.parse(data);  // Throws if invalid

2. Define Both JSON Schema and Zod Schema

// JSON schema for Gemini
const jsonSchema = {
  type: "object",
  properties: {
    name: { type: "string" }
  }
};

// Zod schema for TypeScript
const zodSchema = z.object({
  name: z.string()
});

3. Use Descriptive Field Names

// Good - clear field names
responseJsonSchema: {
  type: "object",
  properties: {
    customerName: { type: "string" },
    orderTotal: { type: "number" },
    shippingAddress: { type: "string" }
  }
}

// Bad - ambiguous
responseJsonSchema: {
  type: "object",
  properties: {
    n: { type: "string" },
    t: { type: "number" },
    a: { type: "string" }
  }
}

4. Provide Context in System Instruction

config: {
  systemInstruction: "Extract product information from user descriptions. Include price as a number without currency symbols.",
  responseMimeType: "application/json",
  responseJsonSchema: { ... }
}

Key Takeaways

  • Gemini has native JSON support via responseMimeType
  • Use responseJsonSchema to guide output structure
  • Always validate with Zod for TypeScript type safety
  • Response is returned directly in response.text
  • Much simpler than Anthropic's prompt-based approach
  • Similar reliability to OpenAI's JSON mode

Next Steps

Learn how to give Gemini access to external tools and APIs!

Next: Lesson 2g - Function/Tool Calling


Quick Reference

// Define Zod schema
const Schema = z.object({
  field: z.string()
});

// Request JSON with schema
const response = await gemini.models.generateContent({
  model: "gemini-3-flash-preview",
  contents: "Extract data...",
  config: {
    responseMimeType: "application/json",
    responseJsonSchema: {
      type: "object",
      properties: {
        field: { type: "string" }
      },
      required: ["field"]
    }
  }
});

// Parse and validate
const data = JSON.parse(response.text);
const validated = Schema.parse(data);

Common Pitfalls

  1. Forgetting responseMimeType: "application/json"
  2. Not validating response with Zod
  3. Mismatched JSON schema and Zod schema
  4. Not handling JSON parsing errors
  5. Using inconsistent field naming conventions