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
| Feature | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| Native JSON mode | Yes | No | Yes |
| Schema support | Limited | No (prompt only) | Yes |
| Reliability | High | Medium | High |
| Config location | response_format | Prompt | config.responseMimeType |
| Needs parsing | Yes | Yes + cleanup | Yes |
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
responseJsonSchemato 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
- Forgetting
responseMimeType: "application/json" - Not validating response with Zod
- Mismatched JSON schema and Zod schema
- Not handling JSON parsing errors
- Using inconsistent field naming conventions