◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-04-17Beginner

Gemini API Returns Markdown — How to Get Plain Text Responses

Gemini API responses often contain Markdown symbols like **, ##, and -. Learn how to get clean plain text using response_mime_type, System Instructions, and post-processing with practical Python and TypeScript code examples.

gemini-api286troubleshooting87markdownplain-textresponsesystem-instructions4python108fix10

You send a prompt to the Gemini API expecting clean text, and instead get back something littered with **bold**, ## headers, and - bullet points. If you are building anything beyond a standard chat UI — a text-to-speech pipeline, a PDF exporter, a database ingestion system — those Markdown symbols cause real problems downstream.

I ran into this firsthand while building a mobile app that passed Gemini responses directly into a TTS engine. The synthesizer dutifully read out "asterisk asterisk" at every bold phrase. Not ideal. Here is what actually works.

Why Gemini API Returns Markdown by Default

Gemini 2.x models are trained to produce readable, well-structured responses. Without explicit instructions, the model uses Markdown to organize information — particularly for explanatory, list-based, or comparative prompts.

Prompt types that reliably trigger Markdown output:

  • "Tell me about X" or "Summarize X" — high chance of headers and bullet lists
  • "List the steps / advantages / differences" — almost always returns formatted lists
  • "Compare X and Y" — typically uses bold labels and structured sections

Prompt types where Markdown is rare:

  • "Translate this text to French"
  • "Fix the grammar in this paragraph"
  • "What is 142 times 37?"

The model decides whether to use Markdown based on what kind of response seems most appropriate for the task. That means you cannot rely on prompt phrasing alone to prevent formatting — you need to set explicit constraints.

Method 1: Set response_mime_type to "text/plain"

This is the simplest and most reliable fix. Setting response_mime_type in GenerationConfig instructs the model to produce plain text output rather than Markdown-formatted content.

Python

import google.generativeai as genai
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
 
model = genai.GenerativeModel(
    model_name="gemini-2.5-pro",
    generation_config=genai.GenerationConfig(
        response_mime_type="text/plain",  # The key setting
    )
)
 
response = model.generate_content(
    "What are the three main advantages of cloud computing?"
)
print(response.text)
# Output: "1. Cost reduction: No upfront infrastructure investment required...
#          2. Scalability: Resources scale on demand..."
# No asterisks, no hash symbols.

Node.js / TypeScript

import { GoogleGenerativeAI } from "@google/generative-ai";
 
const genAI = new GoogleGenerativeAI("YOUR_GEMINI_API_KEY");
 
const model = genAI.getGenerativeModel({
  model: "gemini-2.5-pro",
  generationConfig: {
    responseMimeType: "text/plain", // camelCase in the TypeScript SDK
  },
});
 
const result = await model.generateContent(
  "What are the three main advantages of cloud computing?"
);
console.log(result.response.text());
// Clean plain text, no Markdown formatting

One important clarification: setting response_mime_type: "text/plain" does not degrade response quality. The content remains equally detailed. Bullet points become numbered sentences; bold emphasis disappears into the surrounding text. You lose formatting, not information.

This setting also does not change your token usage. You are paying for the same content either way.

Method 2: Prohibit Markdown via System Instructions

For longer conversations or finer control over output style, combine response_mime_type with a System Instruction that explicitly bans Markdown. The System Instruction defines constraints before the conversation starts, making them more persistent than in-prompt requests.

import google.generativeai as genai
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
 
system_prompt = """You are a helpful assistant.
Follow these output rules at all times:
- Never use Markdown formatting. No asterisks, hash symbols, or backticks.
- For lists, use numbered format: 1. 2. 3.
- For code samples, use indentation only, no code fence syntax.
- Separate paragraphs with blank lines, not headers.
"""
 
model = genai.GenerativeModel(
    model_name="gemini-2.5-pro",
    system_instruction=system_prompt,
    generation_config=genai.GenerationConfig(
        response_mime_type="text/plain",
    )
)
 
response = model.generate_content("How do I reverse a list in Python?")
print(response.text)
# Output:
# There are three common ways to reverse a list in Python.
#
# 1. Using the reversed() function, which returns an iterator:
#    original = [1, 2, 3, 4, 5]
#    reversed_list = list(reversed(original))
#
# 2. Using slice notation...

System Instructions are highly effective for this purpose. However, they are guidance, not a hard constraint baked into the model. On complex prompts or in very long conversations, Markdown can occasionally reappear. That makes Method 3 worth keeping in your stack as a fallback.

Method 3: Strip Markdown in Post-Processing

The most defensible approach is to treat plain text output as something you actively enforce, not merely request. A Markdown stripping function applied to every response catches whatever the model settings miss.

import re
 
def strip_markdown(text: str) -> str:
    """Convert Gemini API Markdown output to plain text."""
    # Remove headings (# ## ###)
    text = re.sub(r'^\#{1,6}\s+', '', text, flags=re.MULTILINE)
    
    # Remove **bold** and *italic* (preserving the text)
    text = re.sub(r'\*{1,2}([^*]+)\*{1,2}', r'\1', text)
    
    # Remove __bold__ and _italic_
    text = re.sub(r'_{1,2}([^_]+)_{1,2}', r'\1', text)
    
    # Remove code block delimiters
    text = re.sub(r'```[a-z]*\n?', '', text)
    text = re.sub(r'```', '', text)
    
    # Remove inline code delimiters (preserving the code text)
    text = re.sub(r'`([^`]+)`', r'\1', text)
    
    # Convert bullet points to a neutral symbol
    text = re.sub(r'^[\-\*]\s+', '• ', text, flags=re.MULTILINE)
    
    # Remove horizontal rules
    text = re.sub(r'^-{3,}$', '', text, flags=re.MULTILINE)
    
    # Compress multiple consecutive blank lines
    text = re.sub(r'\n{3,}', '\n\n', text)
    
    return text.strip()
 
# Example usage
response = model.generate_content("Explain Python list comprehensions")
clean_text = strip_markdown(response.text)
print(clean_text)

TypeScript Version

function stripMarkdown(text: string): string {
  return text
    .replace(/^\#{1,6}\s+/gm, "")              // Remove headings
    .replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1")   // Remove bold/italic markers
    .replace(/_{1,2}([^_]+)_{1,2}/g, "$1")     // Remove underscore bold/italic
    .replace(/```[\w]*
?/g, "")               // Remove code block open markers
    .replace(/```/g, "")                       // Remove code block close markers
    .replace(/`([^`]+)`/g, "$1")               // Remove inline code markers
    .replace(/^[\-\*]\s+/gm, "• ")             // Convert bullets to symbols
    .replace(/^-{3,}$/gm, "")                  // Remove horizontal rules
    .replace(/
{3,}/g, "
 
")                // Compress consecutive blank lines
    .trim();
}
 
const response = await model.generateContent("Explain Python list comprehensions");
const cleanText = stripMarkdown(response.response.text());
console.log(cleanText);

One limitation to be aware of: these regular expressions also remove Markdown delimiters from within code blocks. If your use case requires preserving actual code while stripping surrounding Markdown, you need a more sophisticated parser. For standard plain-text output scenarios — TTS, PDF generation, database storage — this simple approach works reliably.

Handling Multi-Turn Chat Sessions

Markdown re-emergence is most common in multi-turn conversations. As conversation history grows, the model tends to drift back toward formatted output even when response_mime_type is set. This is one of the subtler failure modes that catches developers off guard.

import google.generativeai as genai
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
 
model = genai.GenerativeModel(
    model_name="gemini-2.5-pro",
    system_instruction="Respond in plain text only. No Markdown formatting of any kind.",
    generation_config=genai.GenerationConfig(
        response_mime_type="text/plain",
    )
)
 
chat = model.start_chat(history=[])
 
# Turn 1 — plain text, as expected
response1 = chat.send_message("What is Python?")
clean1 = strip_markdown(response1.text)
print(clean1)
 
# Turn 5 — Markdown may creep back in; post-processing catches it
response5 = chat.send_message("Can you give me a detailed breakdown of its key features?")
clean5 = strip_markdown(response5.text)
print(clean5)

The most reliable configuration for chat sessions: set both response_mime_type and system_instruction on the model, then apply strip_markdown() to every response individually.

When You Actually Want Markdown

Before stripping Markdown everywhere, it is worth asking whether you actually need to. If your frontend renders Markdown — a React component using react-markdown, a mobile app with a Markdown renderer, a documentation site — then the default output is exactly what you want, and stripping it would be counterproductive.

Strip Markdown when the output is going to:

  • A TTS / speech synthesis engine
  • A database field that stores raw text
  • A PDF or Word document via a template
  • An email body (most email clients do not render Markdown)
  • A native mobile UI label that displays literal text

Keep Markdown when the output is going to:

  • A web chat UI with a Markdown rendering library
  • A developer-facing response (Markdown displays nicely in most terminals and IDEs)
  • A rich text editor that accepts Markdown input

Which Approach to Use When

For single-turn API calls, response_mime_type: "text/plain" is almost always sufficient. I skip the System Instruction to save tokens.

For TTS pipelines, I use all three layers: response_mime_type, System Instruction, and strip_markdown(). A TTS engine reading "asterisk asterisk important" aloud is a user experience failure, so the extra overhead is worth it.

For multi-turn chat sessions, I set both response_mime_type and System Instruction up front, then apply strip_markdown() to each response. The combination handles even long sessions reliably.

Wrapping Up

Add response_mime_type: "text/plain" to your GenerationConfig first. That single line resolves the problem for the majority of cases. For TTS, PDF export, or long chat sessions, layer in a System Instruction and the strip_markdown() utility to cover edge cases.

Once you have plain text output working reliably, the next useful experiment is the opposite direction: getting consistently structured JSON using response_mime_type: "application/json" paired with response_schema. That combination handles data extraction and transformation tasks far better than trying to parse structured information out of free-form text.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◈ API / SDK2026-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.
◈ API / SDK2026-05-17
Diagnosing INVALID_ARGUMENT Errors in Gemini API Batch Image Analysis
When using the Gemini API to analyze multiple images at once, INVALID_ARGUMENT errors can be surprisingly hard to diagnose. This guide covers the three root causes—MIME type mismatches, inline data size limits, and contents structure errors—with code to fix each one.
◈ API / SDK2026-09-04
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.