How to force Gemini API to return strict JSON in Node.js

Learn how to use the responseSchema parameter in the Gemini API to guarantee valid, structured JSON output. Avoid markdown formatting issues in your Node.js apps.

You ask an AI model for a JSON response, and it gives you a markdown string starting with backticks and the word json. Your scraper or bot tries to run JSON.parse() on the output and immediately crashes.

We can fix this by forcing the Gemini API to return strict, raw JSON every time. Instead of trying to clean up the string with regular expressions, we will configure the Node.js SDK to lock the output format to a specific schema.

Setting up the Gemini SDK

First, you need the official Google Gen AI SDK for Node.js. If you are using the older version, update to the current @google/generative-ai package. You will also need your API key from the Google AI Studio dashboard. Click Get API key to generate a new one if you do not have it yet.

npm install @google/generative-ai

You must pass your API key into the client as an environment variable or a direct string. Keep this key secure. If you are building a web app, do not expose it to the browser.

Defining the output schema

To get strict JSON, pass two specific properties in the generationConfig object of your request: responseMimeType and responseSchema.

The responseMimeType tells the model to drop all conversational text and markdown formatting. Setting it to application/json ensures the raw string returned is just the JSON structure.

The responseSchema tells the model exactly what keys and data types to use. Gemini uses a subset of the OpenAPI schema format. You build this using the SchemaType enum provided by the SDK.

Here is an example schema for extracting product details from a block of text. We want an array of objects, where each object has a name, price, and category.

import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

const productSchema = {
  type: SchemaType.ARRAY,
  description: "A list of products extracted from the text",
  items: {
    type: SchemaType.OBJECT,
    properties: {
      productName: {
        type: SchemaType.STRING,
        description: "The official name of the product",
      },
      price: {
        type: SchemaType.NUMBER,
        description: "The price in USD",
      },
      inStock: {
        type: SchemaType.BOOLEAN,
        description: "True if the item is available",
      },
      category: {
        type: SchemaType.STRING,
        enum: ["electronics", "clothing", "home", "other"],
        description: "The product category",
      }
    },
    required: ["productName", "price", "category"],
  },
};

Using enum restricts the model to a predefined list of strings. Providing a clear description for each field acts as an inline prompt, guiding the model on what specific data belongs in that key.

Sending the request

Pass the schema to the model instance. You must use a model that supports structured outputs, such as Gemini 1.5 Pro or Gemini 1.5 Flash.

async function extractProducts(text) {
  const model = genAI.getGenerativeModel({
    model: "gemini-1.5-flash",
    generationConfig: {
      responseMimeType: "application/json",
      responseSchema: productSchema,
    },
  });

  const prompt = `Extract all the products from this text: \n\n ${text}`;

  const result = await model.generateContent(prompt);
  const response = result.response;
  
  const rawText = response.text();
  const parsedData = JSON.parse(rawText);
  
  return parsedData;
}

// Example usage
const sourceText = "We have a new Sony TV for 499.99. It is currently in stock. Also selling a cotton t-shirt for 15, but we are sold out.";

extractProducts(sourceText).then((data) => {
  console.log(data);
});

Because we set the MIME type, the response.text() method returns a clean JSON string. There are no backticks. You can immediately pass it to JSON.parse() without any string manipulation.

Handling complex nested structures

You can nest objects and arrays as deep as you need. However, every level of an array must define the items property, and every object must define the properties property.

If you want a response that includes metadata alongside the main data array, you start the schema as an object.

const documentSchema = {
  type: SchemaType.OBJECT,
  properties: {
    summary: {
      type: SchemaType.STRING,
      description: "A one sentence summary of the entire text",
    },
    tags: {
      type: SchemaType.ARRAY,
      items: {
        type: SchemaType.STRING,
      },
    },
    products: productSchema, // Reusing the schema from earlier
  },
  required: ["summary", "products"],
};

This modular approach makes it easier to manage large schemas. Build smaller pieces and reference them inside the main response schema.

Things that bit me

The model will still occasionally hallucinate keys if you do not strictly define the required array in your schema. If a field is optional, the model might omit it entirely or return null, which can break downstream code expecting a specific type.

Setting responseMimeType: 'application/json' without providing a responseSchema will return JSON, but the model decides the structure. It will change the keys based on the input text, making the output impossible to rely on for automated scripts.

Large outputs can get cut off if you hit the maxOutputTokens limit. The response will end in the middle of a string, and JSON.parse() will throw an error. You must check the finish reason in the response object or catch the parse error to detect this.

If your prompt explicitly tells the model to format the output as markdown, it will fight the SDK configuration. It might return invalid JSON that includes markdown syntax inside the string values.

If you need help building reliable extractions, you can check out other things I build.

FAQ

Quick answers

How do I stop Gemini from returning markdown code blocks?

Configure the generation settings with responseMimeType set to application/json. This tells the API to return the raw string without wrapping it in markdown backticks.

Do I need to provide a response schema?

If you only set the MIME type, the model will return JSON but it will invent its own keys. Providing a responseSchema forces the model to use the exact keys and data types you define.

Why does JSON.parse fail on the Gemini response?

The output might be cut off if it exceeds the maximum output tokens. Also, if your prompt contradicts the JSON settings, the model might include invalid characters inside the string.