Models try to follow your format. They mostly succeed. They occasionally don't — "Here's the JSON: {...}" or markdown-fenced code blocks instead of bare JSON. Your code has to handle both.
import json
from pydantic_ai import Agent
def classify_with_retry(text):
prompt = f'Classify "{text}" — return JSON {{"sentiment": "..."}}. Return only valid JSON.'
for attempt in range(2):
result = Agent(model).run_sync(prompt)
try:
return json.loads(result.output.strip())
except json.JSONDecodeError:
prompt = prompt + "\n\nReturn ONLY valid JSON. No markdown, no prose."
raise ValueError("model failed to return valid JSON after 2 attempts")A retry that reinforces the format constraint?
Yes — the second attempt's prompt nudges harder. Often that's enough. If it still fails, raise rather than swallow — silent fallback is worse than a clear error.
try/except json.JSONDecodeErrorThe robust pattern:
import json
for attempt in range(2):
result = Agent(model).run_sync(prompt)
try:
parsed = json.loads(result.output.strip())
break
except json.JSONDecodeError:
prompt += "\n\nReturn ONLY valid JSON. No markdown, no prose."
else:
raise ValueError("model failed to return JSON")The for/else construct runs the else block when the loop completes without break — clean way to express "exhausted retries".
| Slip | Output |
|---|---|
| Markdown fence | ```json\n{...}\n``` |
| Prose preamble | "Here's the JSON: {...}" |
| Trailing commentary | {...} - this classifies as positive. |
| Smart quotes instead of straight | {"sentiment": "positive"} (different quote chars — still a parse error) |
"Return ONLY valid JSON, no prose, no markdown" — works ~90% of the time.result.output.strip().lstrip('```json').rstrip('```').strip() — helps when the model uses fences but is otherwise valid.output_type=YourModel handles parsing internally and retries automatically. The cleaner production answer.If 2-3 retries fail, the prompt itself is the problem. Either the task is too ambiguous, or the model is too small. Switch model or simplify the schema before adding more retries.
Models try to follow your format. They mostly succeed. They occasionally don't — "Here's the JSON: {...}" or markdown-fenced code blocks instead of bare JSON. Your code has to handle both.
import json
from pydantic_ai import Agent
def classify_with_retry(text):
prompt = f'Classify "{text}" — return JSON {{"sentiment": "..."}}. Return only valid JSON.'
for attempt in range(2):
result = Agent(model).run_sync(prompt)
try:
return json.loads(result.output.strip())
except json.JSONDecodeError:
prompt = prompt + "\n\nReturn ONLY valid JSON. No markdown, no prose."
raise ValueError("model failed to return valid JSON after 2 attempts")A retry that reinforces the format constraint?
Yes — the second attempt's prompt nudges harder. Often that's enough. If it still fails, raise rather than swallow — silent fallback is worse than a clear error.
try/except json.JSONDecodeErrorThe robust pattern:
import json
for attempt in range(2):
result = Agent(model).run_sync(prompt)
try:
parsed = json.loads(result.output.strip())
break
except json.JSONDecodeError:
prompt += "\n\nReturn ONLY valid JSON. No markdown, no prose."
else:
raise ValueError("model failed to return JSON")The for/else construct runs the else block when the loop completes without break — clean way to express "exhausted retries".
| Slip | Output |
|---|---|
| Markdown fence | ```json\n{...}\n``` |
| Prose preamble | "Here's the JSON: {...}" |
| Trailing commentary | {...} - this classifies as positive. |
| Smart quotes instead of straight | {"sentiment": "positive"} (different quote chars — still a parse error) |
"Return ONLY valid JSON, no prose, no markdown" — works ~90% of the time.result.output.strip().lstrip('```json').rstrip('```').strip() — helps when the model uses fences but is otherwise valid.output_type=YourModel handles parsing internally and retries automatically. The cleaner production answer.If 2-3 retries fail, the prompt itself is the problem. Either the task is too ambiguous, or the model is too small. Switch model or simplify the schema before adding more retries.
Create a free account to get started. Paid plans unlock all tracks.