Strip Markdown Code Fences from JSON: Complete Guide
AI models wrap JSON in markdown code fences that break JSON.parse. Learn to strip them with regex in JavaScript and Python, and automate it in n8n.
Have broken JSON right now? Fix it free in under 1 second — no signup.
Fix My JSON →Every developer who has called an AI API has run into this problem. You ask the model to return JSON. It does, technically, but it wraps the whole thing in a markdown code fence. Now JSON.parse throws a SyntaxError, and you're left wondering why a model that can write code can't just return plain JSON.
This guide covers why it happens, how to strip markdown fences reliably in JavaScript and Python, how to handle the tricky edge cases, and how to automate the fix in workflow tools like n8n and Make.
Why AI Models Add Markdown Fences
AI models are trained on enormous amounts of text from the internet, including GitHub READMEs, Stack Overflow answers, documentation sites, and tutorials. In all of that text, JSON appears inside markdown code fences all the time:
`
json
{
"name": "Alice",
"age": 30
}
`
The model learns that "when showing JSON, wrap it in a code fence." That's actually the correct behavior in a markdown context, where formatting makes the output readable. The problem is the model applies this pattern even when you ask for raw JSON for programmatic use.
The model doesn't distinguish between "show me JSON to read" and "give me JSON to parse." From the model's perspective, it's providing a nicely formatted, syntax-highlighted JSON block. From your code's perspective, it's garbage.
What the Output Actually Looks Like
Here are the real patterns you'll encounter from different AI APIs.
Standard Code Fence
The most common pattern:
json
{"name": "Alice", "age": 30}
The fence starts with three backticks and optionally a language identifier (json, JSON, js, or nothing). It ends with three backticks on their own line.
Fence with Prose Before and After
More common in chat completions than structured API calls:
Here is the JSON data you requested:json
{
"user": {
"name": "Alice",
"age": 30
}
}
Let me know if you need any changes!
Your strip function needs to handle not just the fences but the surrounding prose.
Tilde Fences
Some models and markdown renderers use tildes instead of backticks:
~~~json
{"name": "Alice"}
~~~
Less common but real. Your regex should handle both.
No Language Identifier
The fence exists but without json:
{"name": "Alice", "age": 30}
Nested Fences
The model is asked to return JSON that itself contains a markdown field with code fences in it. This is the tricky case covered in the edge cases section.
How to Strip Markdown Fences in JavaScript
Simple Case: Just the Fence
If your AI response is always just a code fence with no surrounding prose:
function stripCodeFence(raw) {
return raw
.replace(/^
(?:json)?\s*\n?/i, '') // opening fence
.replace(/\n?```\s*$/m, '') // closing fence
.trim();
}
const raw = "``json\n{\"name\": \"Alice\", \"age\": 30}\n``";
const json = JSON.parse(stripCodeFence(raw));
console.log(json); // { name: 'Alice', age: 30 }
Extracting JSON from Prose
When the response includes text before and after the JSON block:
javascript
function extractJsonFromMarkdown(raw) {
// Try to find a code fence first
const fenceMatch = raw.match(/``(?:json|JSON)?\s([\s\S]?)``/);
if (fenceMatch) {
return fenceMatch[1].trim();
}
// Try tilde fences
const tildeMatch = raw.match(/~~~(?:json|JSON)?\s([\s\S]?)~~~/);
if (tildeMatch) {
return tildeMatch[1].trim();
}
// No fence found - extract the first JSON object or array
const start = raw.search(/[{\[]/);
const end = Math.max(raw.lastIndexOf('}'), raw.lastIndexOf(']'));
if (start !== -1 && end !== -1) {
return raw.slice(start, end + 1).trim();
}
// Return as-is and let the caller handle the error
return raw.trim();
}
const response = `Here is the extracted data:
\\\`json
{
"title": "Product Launch",
"date": "2024-01-15",
"tags": ["launch", "product"]
}
\\\`
Let me know if you need changes!`;
const jsonStr = extractJsonFromMarkdown(response);
const data = JSON.parse(jsonStr);
console.log(data.title); // "Product Launch"
Production-Ready Version with Error Handling
javascript
function parseAiJsonResponse(raw) {
if (typeof raw !== 'string') {
throw new TypeError(Expected string, got ${typeof raw});
}
let extracted = raw.trim();
// Step 1: strip code fence if present
const fencePattern = /^{3,}(?:json|JSON)?\s\n?([\s\S]?)\n?{3,}\s*$/m;
const fenceMatch = extracted.match(fencePattern);
if (fenceMatch) {
extracted = fenceMatch[1].trim();
} else {
// Step 2: extract JSON block from prose
const firstBracket = extracted.search(/[{\[]/);
const lastClosing = Math.max(
extracted.lastIndexOf('}'),
extracted.lastIndexOf(']')
);
if (firstBracket !== -1 && lastClosing > firstBracket) {
extracted = extracted.slice(firstBracket, lastClosing + 1).trim();
}
}
// Step 3: attempt parse
try {
return JSON.parse(extracted);
} catch (e) {
throw new SyntaxError(
Failed to parse JSON after fence removal: ${e.message}\nExtracted: ${extracted.slice(0, 200)}
);
}
}
// Usage
try {
const result = parseAiJsonResponse(aiResponse);
console.log(result);
} catch (e) {
console.error('Parse failed:', e.message);
}
How to Strip Markdown Fences in Python
Simple Regex Approach
python
import json
import re
def strip_code_fence(raw: str) -> str:
"""Remove markdown code fences from a JSON string."""
# Match opening fence with optional language identifier
raw = re.sub(r'^```(?:json|JSON)?\s*\n?', '', raw.strip())
# Match closing fence
raw = re.sub(r'\n?```\s*$', '', raw, flags=re.MULTILINE)
return raw.strip()
raw = "``json\n{\"name\": \"Alice\", \"age\": 30}\n``"
data = json.loads(strip_code_fence(raw))
print(data) # {'name': 'Alice', 'age': 30}
Extracting from Prose in Python
python
import json
import re
from typing import Any
def extract_json_from_markdown(raw: str) -> Any:
"""Extract and parse JSON from an AI response that may include markdown prose."""
# Try code fence first (backticks)
fence_match = re.search(r'``(?:json|JSON)?\s([\s\S]?)``', raw)
if fence_match:
return json.loads(fence_match.group(1).strip())
# Try tilde fences
tilde_match = re.search(r'~~~(?:json|JSON)?\s([\s\S]?)~~~', raw)
if tilde_match:
return json.loads(tilde_match.group(1).strip())
# Find the first JSON object or array in the text
start = -1
for i, ch in enumerate(raw):
if ch in ('{', '['):
start = i
break
if start == -1:
raise ValueError("No JSON object or array found in response")
end = max(raw.rfind('}'), raw.rfind(']'))
if end == -1 or end < start:
raise ValueError("Could not find closing bracket")
return json.loads(raw[start:end + 1].strip())
Usage
response = """
Here is the user data:
{
"name": "Alice",
"role": "admin",
"active": true
}
"""
data = extract_json_from_markdown(response)
print(data["name"]) # Alice
Python with Logging
For production code where you want to know when fence stripping was needed:
python
import json
import re
import logging
from typing import Any, Optional
logger = logging.getLogger(__name__)
def parse_ai_response(raw: str) -> Optional[Any]:
"""
Parse JSON from an AI response.
Logs a warning if fence stripping was required.
Returns None if parsing fails after all repair attempts.
"""
stripped = raw.strip()
# Check if fence stripping is needed
has_fence = bool(re.search(r'^```', stripped, re.MULTILINE))
if has_fence:
logger.warning("AI response contained markdown fences - stripping before parse")
fence_match = re.search(r'``(?:json|JSON)?\s([\s\S]?)``', stripped)
if fence_match:
stripped = fence_match.group(1).strip()
try:
return json.loads(stripped)
except json.JSONDecodeError as e:
logger.error("JSON parse failed after fence removal: %s", e)
logger.debug("Failed input: %s", stripped[:500])
return None
Edge Cases
Nested Fences: JSON That Contains Markdown
This is the hardest case. You ask the AI to return a JSON object with a markdown field, and the field value contains a code fence:
{
"title": "Example",
"content": "Here is code:\n
python\nprint('hello')\n```\n"
}
A naive regex that matches "three backticks to three backticks" will match the opening `json to the first closing ` inside the string value, breaking the extraction.
The solution is to match the specific opening fence pattern and find the corresponding closing fence:
function stripOuterFenceOnly(raw) {
// Match the outermost fence only - closing fence must be on its own line
// preceded by an optional newline and with nothing else on that line
const outerFence = /^(`{3,})([^\n])\n([\s\S]?)\n\1\s*$/;
const match = raw.trim().match(outerFence);
if (match) {
return match[3].trim();
}
return raw.trim();
}
The key insight is \1 (backreference) which requires the closing fence to have the same number of backticks as the opening fence. This correctly handles nested fences that use a different number of backticks.
Partial Fence Stripping
Sometimes the model produces only half a fence, opening but no closing:
json
{"name": "Alice", "age": 30}
(The AI stopped generating before it added the closing fence.)
Your regex won't match because there's no closing fence. The fallback: strip the opening fence and extract the JSON block by bracket matching:
javascript
function robustExtract(raw) {
let s = raw.trim();
// Remove partial opening fence
s = s.replace(/^```(?:json)?\s*\n?/im, '');
// Find complete JSON block
const start = s.search(/[{\[]/);
const end = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
if (start !== -1 && end !== -1) {
return s.slice(start, end + 1).trim();
}
return s.trim();
}
Multiple JSON Blocks
The model returns multiple code fences with different JSON objects:
First user:
{"name": "Alice"}
Second user:
{"name": "Bob"}
Your extraction will only capture the first block by default. If you need all blocks:
javascript
function extractAllJsonBlocks(raw) {
const results = [];
const pattern = /``(?:json|JSON)?\s([\s\S]?)``/g;
let match;
while ((match = pattern.exec(raw)) !== null) {
try {
results.push(JSON.parse(match[1].trim()));
} catch (e) {
// Skip invalid blocks
}
}
return results;
}
Unicode and BOM
Occasionally AI APIs return a UTF-8 BOM (byte order mark, \uFEFF) at the start of the response, or other invisible characters. These break both fence matching and JSON parsing.
Add this as a first step in your extraction:
javascript
function cleanRawResponse(raw) {
return raw
.replace(/^\uFEFF/, '') // BOM
.replace(/\u200B/g, '') // zero-width space
.replace(/\u00A0/g, ' ') // non-breaking space
.trim();
}
n8n Workflow Code for Fence Stripping
In n8n, use a Code node immediately after your AI node. This handles all the patterns described above:
javascript
// n8n Code node - strip markdown fences from AI output
const raw = $input.first().json.text
?? $input.first().json.output
?? $input.first().json.message?.content
?? '';
function extractJson(input) {
let s = input.trim();
// Remove BOM and invisible chars
s = s.replace(/^\uFEFF/, '').replace(/\u200B/g, '');
// Try fence extraction
const fenceMatch = s.match(/``(?:json|JSON)?\s([\s\S]?)``/);
if (fenceMatch) {
s = fenceMatch[1].trim();
} else {
// Fallback: find JSON by brackets
s = s.replace(/^```(?:json)?\s*\n?/im, '');
const start = s.search(/[{\[]/);
const end = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
if (start !== -1 && end !== -1) s = s.slice(start, end + 1);
}
return s.trim();
}
const extracted = extractJson(raw);
try {
return [{ json: JSON.parse(extracted) }];
} catch (e) {
return [{ json: { _parseError: e.message, _raw: extracted } }];
}
Connect this Code node between your OpenAI/Anthropic node and any downstream nodes that expect structured data.
Make (Integromat) Workflow Code
In Make, you can use the Tools > Text parser module with a regex to extract the JSON content, then parse it with the Tools > Parse JSON module.
Alternatively, use an HTTP module to call AI JSONMedic's repair API, which handles fence stripping along with other repair operations:
HTTP module settings:
- URL:
https://aijsonmedic.com/api/repair - Method: POST
- Headers:
Content-Type: application/json - Body (raw):
{"input": "{{1.text}}"}
The response contains output (clean JSON string) and issues (list of repairs applied). Pass output to a Parse JSON module.
For more Make-specific JSON error handling, see our Make and Integromat JSON errors guide.
Using AI JSONMedic for Fence Stripping
AI JSONMedic handles markdown fence stripping as one of its first repair steps. Paste any AI-generated response, even with prose and fences, and it extracts and repairs the JSON in one pass.
The repair report tells you exactly what was found and removed, so you can verify the extraction was correct. It also handles the other common AI JSON errors (trailing commas, Python literals, truncation) in the same pass, which matters because AI responses often have multiple issues at once.
For bulk operations or automated pipelines, use the API endpoint:
javascript
const response = await fetch('https://aijsonmedic.com/api/repair', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: rawAiResponse }),
});
const { output, issues } = await response.json();
// output: clean JSON string
// issues: array like ["Stripped markdown fence", "Removed trailing comma"]
const data = JSON.parse(output);
FAQ
Q: Should I fix this with a regex or a proper JSON parser?
For fence stripping specifically, a regex is appropriate because the fence is outside the JSON, not inside it. A proper lenient JSON parser (like JSON5) handles errors inside the JSON. Use regex for the fence, then pass the result to a lenient parser if you still have issues inside the JSON.
Q: Can I just tell the AI not to use code fences?
You can reduce the frequency but not eliminate it. Add "Return only raw JSON, no markdown, no code fences" to your prompt. With OpenAI's JSON mode (response_format: { type: "json_object" }), fences are eliminated entirely. But JSON mode isn't available on all models, and other AI providers vary.
Q: The model wraps the JSON in a single backtick instead of three. How do I handle that?
Single-backtick inline code (` {"name":"Alice"} `) is less common but appears occasionally. Add a stripping step for it:
javascript
s = s.replace(/^([^]+)`$/, '$1');
```
Q: I'm getting a "maximum call stack" error with my recursive fence-matching code. What went wrong?This usually means the regex is catastrophically backtracking on a large input. Avoid regex patterns with nested quantifiers on unknown-length input. Use the greedy [\s\S]? (non-greedy, any character including newlines) version instead of . with s flag.
No. Fence stripping removes the wrapper. If the JSON inside has trailing commas, unquoted keys, or other errors, you need a separate repair step. Use AI JSONMedic which does both in one pass, or chain a fence-strip step with a repair step in your code.
Q: My response uses four backticks instead of three. Is that valid markdown?Yes. Markdown allows any number of backticks (3 or more) as a fence delimiter. Some AI models use four to allow nested triple-backtick code inside the fence. The backreference approach (\1 in the regex) handles any number correctly.
Conclusion
Markdown code fences are a near-universal problem when using AI APIs. The model is doing the right thing for human readers and the wrong thing for parsers.
The fix is a two-step regex: match and capture the content between the opening and closing fence. The edge cases (nested fences, partial fences, multiple blocks) are where simple regexes break down, and the solutions above handle those correctly.
For a zero-code solution, AI JSONMedic handles fence stripping and any other AI JSON errors in one paste. For code solutions, the extractJsonFromMarkdown functions in this article are production-tested patterns. See also how to fix ChatGPT JSON errors and fix n8n JSON parse errors for the full context of where fence stripping fits in your AI pipeline.
Still dealing with broken JSON?
Paste it in and get it fixed in under 1 second — free, no signup, no install. Works with ChatGPT, Claude, n8n, and any AI output.
Fix My JSON Free →Related Articles