I want to show you every way an LLM can break your JSON.parse call, and how to handle all of them in one line.
The many ways LLMs break JSON
If you're calling OpenAI, Claude, Gemini, Ollama, or any other LLM and asking for JSON, here's what you'll eventually get back:
1. Markdown code fences
The most common one. You ask for JSON, the model gives you a helpful little markdown block:
Sure! Here you go:
```
{% endraw %}
json
{"score": 95}
{% raw %}
**2. Trailing commas**
Models love trailing commas. Especially in arrays.
```json
{"items": ["a", "b", "c",], "count": 3,}
3. Unquoted keys
The model writes JavaScript instead of JSON:
{name: "Alice", age: 30, active: true}
4. Single quotes
{'name': 'Alice', 'city': 'New York'}
5. Smart quotes and em dashes
Copy-paste artifacts or just models being fancy:
{"name": "Alice", "range": "10—20"}
Those " and — characters look right but they are not ASCII and JSON.parse will reject them.
6. Inline comments
{
"name": "Alice", // the user
"age": 30 /* years */
}
7. JSON buried in a paragraph
Based on the analysis, the result is {"score": 87, "pass": true} for this input.
8. Invisible unicode
UTF-8 BOM (\uFEFF) at the start, zero-width spaces (\u200B) between characters. You can't see them. Your parser can.
The one-line fix
npm install ai-json-safe-parse
import { aiJsonParse } from 'ai-json-safe-parse'
const result = aiJsonParse<{ score: number }>(llmResponse)
if (result.success) {
console.log(result.data.score)
} else {
console.log(result.error)
}
That handles every case above. It runs a recovery pipeline — tries direct parse first, then strips markdown, normalizes unicode, does bracket matching to extract JSON from prose, and if needed fixes trailing commas, quotes, keys, and comments.
It returns a typed discriminated union. Never throws. If you want simpler APIs:
// Returns T | null
const data = aiJsonSafeParse<MyType>(llmResponse)
// Returns T or your fallback
const data = aiJsonSafeParse(llmResponse, { score: 0 })
// Throws on failure
const data = aiJsonStrictParse<MyType>(llmResponse)
Safe vs aggressive
By default it uses aggressive mode — it will actually repair broken syntax. If you only want extraction (markdown stripping, bracket matching) without modifying the JSON:
const result = aiJsonParse(text, { mode: 'safe' })
Details
- Zero dependencies
- ~2KB gzipped
- TypeScript with full generics
- ESM and CommonJS
- Works in Node.js, browsers, Cloudflare Workers, Deno
GitHub: a-r-d/ai-json-safe-parse
npm: ai-json-safe-parse
If you hit a format it doesn't handle, open an issue with the raw text.
Top comments (0)