I tried for more than an hour to prompt and system promt Gemini to not wrap it’s response in markdown or markdown code blocks. No matter what it is completely hit or miss. You cannot rely on Gemini returning clean text, json, html, or any other format.
So, I’m now putting this node immediately after each Gemini model response.
It will extract the output from the markdown codeblock if it exists. If not, it will pass the response as is.
const llmMarkdown = $input.first()?.json?.output ?? "";
// Find the content inside the markdown code block.
const match = llmMarkdown.match(/```(?:[a-zA-Z]+)?\n([\s\S]*?)\n```/);
const llmText = match ? match[1].trim() : llmMarkdown;
try {
// Try parsing as JSON
return [{ json: {output: JSON.parse(llmText) }}];
} catch {
// If it's not JSON, return as is.
return [{ json: { output: llmText } }];
}
The solution I shared isn’t tied to any specific structure—it’s just meant for cases where the model responds with something inside markdown code blocks.
That could be:
```json
```html
```text
etc.
I was just looking to capture whatever is inside the code block, not enforce a particular format.
And, If the response isn’t in a code block, to get the response as is.
Or, @Tero, maybe I’m misunderstanding—does your approach handle this as well? It looks like your approach is aiming for a specifically structured response inside code blocks.
Can you please give an example as I am getting json markdown sometimes and sometimes without markdown . Without markdown gets processed easily the markdown one creates issues.
Absolute legend! I was struggling with the same issue using the latest N8N and Gemini 2.5. They REALLY need to fix the structured output node so that it actually uses the functionality offered by Google and OpenAI instead of the now, very outdated method of using langchain in the background to prompt an output structure…
Thanks @elabbarw. Glad to help.
I don’t see a way to edit the OP any longer, so posting an update here. I found that Gemini will often incorrectly escape single quotes and it would lead to extraction errors. This version provides a fix.
const prevNodeName = $prevNode?.name ?? 'Unknown';
const output = $input.first()?.json?.output ?? '';
if (!output) {
throw new Error(`No input data found from previous node ${prevNodeName}`);
}
const match = output.match(/```(?:[a-zA-Z]+)?\n([\s\S]*?)\n```/);
const jsonString = match ? match[1].trim() : output.trim();
// Clean the common LLM error of escaping single quotes
const cleanedJsonString = jsonString.replace(/\\'/g, "'");
try {
const parsedJson = JSON.parse(cleanedJsonString);
return [{ json: parsedJson }];
} catch (error) {
throw new Error(
`Failed to parse output as JSON from previous node ${prevNodeName}: ${error.message}`,
{
cause: {
original_text: jsonString,
},
},
);
}
After running this on tens-of-thousands of outputs I ran into an infrequent but reproducible error every once in a while. Sometimes Gemini doesn’t put the closing ``` at the end of it’s output. This version now handles that case.
const prevNodeName = $prevNode?.name ?? 'Unknown';
const output = $input.first()?.json?.output ?? '';
if (!output) {
throw new Error(`No input data found from previous node ${prevNodeName}`);
}
// Try parsing the whole output as JSON first.
let jsonString;
try {
JSON.parse(output.trim());
jsonString = output.trim();
} catch {
// Look for fenced code blocks like ```json\n{...}\n```
const fencedMatch = output.match(/```(?:[a-zA-Z]+)?\s*\r?\n([\s\S]*?)\r?\n?```/);
if (fencedMatch) {
jsonString = fencedMatch[1].trim();
} else {
// Extract first top-level JSON object by balancing braces
const firstBraceIndex = output.indexOf('{');
if (firstBraceIndex === -1) {
jsonString = output.trim();
} else {
let braceDepth = 0;
let inString = false;
let stringQuote = '';
let isEscaped = false;
let endIndex = firstBraceIndex;
for (; endIndex < output.length; endIndex++) {
const ch = output[endIndex];
if (inString) {
if (isEscaped) {
isEscaped = false;
} else if (ch === '\\') {
isEscaped = true;
} else if (ch === stringQuote) {
inString = false;
}
} else {
if (ch === '"' || ch === "'") {
inString = true;
stringQuote = ch;
} else if (ch === '{') {
braceDepth++;
} else if (ch === '}') {
braceDepth--;
if (braceDepth === 0) {
endIndex++; // include closing brace
break;
}
}
}
}
jsonString = output.slice(firstBraceIndex, endIndex).trim();
}
}
}
// Clean the common LLM error of escaping single quotes
const cleanedJsonString = jsonString.replace(/\\'/g, "'");
try {
const parsedJson = JSON.parse(cleanedJsonString);
return [{ json: parsedJson }];
} catch (error) {
throw new Error(
`Failed to parse output as JSON from previous node ${prevNodeName}: ${error.message}`,
{
cause: {
original_text: jsonString,
},
},
);
}