Type-Safe JSON Extraction with Gemini and Schema-First Design

Technologies Used
GeminiJavascript

Type-Safe JSON Extraction with Gemini and Schema-First Design

A common pain point for most developers when first integrating LLMs into their projects is to get an easily parsable set of data from the response. Even if you ask the model to return the response as JSON you will generally find it still returns a standard markdown response with the JSON embedded which will break any functions like JSON.parse() which you want to use to map the response to a useful structure.

We could resort to using regex or by getting more "instructional" with the prompt to fix this but the better option is to define the 'Contract' you want with Gemini using its schema parameter to have the model respond as an API instead of as a chatbot.

The Solution: Schema-First Extraction

Instead of asking the model to "only return JSON," we define a Response Schema. This forces the model’s decoding process to follow a specific structural path.

Here is how we implement your Tag Generator using a clean, modular approach:

import { GeminiClient } from './lib/GeminiClient.js';

/**
 * 1. Define the 'Contract'
 * We use a JSON Schema (OpenAPI 3.0 compliant) to define exactly 
 * what the object should look like.
 */
const tagSchema = {
    type: "object",
    properties: {
        tags: {
            type: "array",
            description: "A list of relevant technology tags.",
            items: { type: "string" },
            maxItems: 3 // Enforce the 'maximum of three' rule at the schema level
        },
    },
    required: ["tags"],
};

/**
 * 2. Define the System Instruction
 * Set the persona and high-level constraints here. 
 * This keeps the User Query clean.
 */
const systemPrompt = {
    parts: [{
        text: "You are a professional content taxonomist. Your goal is to extract technology keywords from technical blog posts."
    }]
};

export async function generateTagsForBlogPost(content) {
    const geminiClient = new GeminiClient();
    
    // We pass the schema directly into the request config
    // This activates 'JSON Mode' in the Gemini API
    const tagResponse = await geminiClient.request(
        systemPrompt, 
        `Content to analyze: ${content}`, 
        tagSchema
    );

    return tagResponse; 
    // No more JSON.parse() headaches or markdown stripping!
}

Our Gemini Client Class

export class GeminiClient {
    async request(systemInstruction, userQuery, responseSchema) {
        const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${process.env.GEMINI_API_KEY}`;
        
        const payload = {
            contents: [{ parts: [{ text: userQuery }] }],
            systemInstruction,
            generationConfig: {
                response_mime_type: "application/json",
                response_schema: responseSchema
            }
        };

        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });

        const result = await response.json();
        return JSON.parse(result.candidates[0].content.parts[0].text);
    }
}

Notice how we set the responseMimeType and responseSchema which ensures Gemini will only show results in the format we have instructed it to.

Why this is an improvement

  1. Schema Validation at the Edge: By passing tagSchema, the Gemini engine uses constrained decoding. It literally cannot generate a token that violates the schema.
  2. Separation of Concerns: * System Prompt: Defines Who the AI is.
  • User Query: Provides the Data.
  • Schema: Defines the Interface.
  1. Predictability: In a Typescript project, you can now confidently type your ref as const tags = ref<{tags: string[]}>().
  2. Token Efficiency Since the model is only returning JSON it will not waste processing time on generating conversational responses which makes it both faster and cheaper

Key Takeaways

  • Stop String-Stripping: If you're still using .replace('```json', ''), you can get more reliable results with schema response structures.
  • Descriptions Matter: Adding a description field inside your JSON Schema helps the model understand the intent of each field better than just the key name.

Explore More Posts 👇