Skip to content

How to Get Structured Outputs from Claude Using Prompt Schemas

Problem

When I first started using Claude for programmatic tasks, I kept running into the same issue. Claude would give me helpful responses, but they were impossible to parse consistently.

I tried asking Claude to analyze error logs:

prompt.txt
Me: Analyze this error and tell me how to fix it.
Error: Connection refused to localhost:5432
at Database.connect (db.js:45)
at Server.start (server.js:12)

Claude responded with:

claude-response.txt
Claude: This error indicates that your application cannot connect to the database.
The connection is being refused on localhost port 5432, which is typically
used by PostgreSQL. Let me explain what's happening...
[Three paragraphs of explanation]
To fix this, you should:
1. Check if PostgreSQL is running
2. Verify your connection string
3. Check firewall settings
[More explanation follows]

This is helpful for reading, but terrible for automation. I wanted to:

  • Feed the response into my CI/CD pipeline
  • Create structured bug reports
  • Build automated debugging tools

But every time I tried to parse Claude’s responses, the format would change slightly. Sometimes Claude used numbered lists. Sometimes bullet points. Sometimes it buried the fix in the middle of a paragraph.

What I tried first

I attempted to extract the information with regex:

parser.py
import re
def parse_claude_response(response):
# Try to find the fix
fix_match = re.search(r'fix[^:]*:\s*(.+)', response, re.IGNORECASE)
if fix_match:
return fix_match.group(1)
# Try to find numbered steps
steps = re.findall(r'\d+\.\s*(.+)', response)
if steps:
return steps
return None

This worked sometimes. But when Claude changed formats, my parser broke:

format-variations.txt
# Claude sometimes uses:
"To fix this issue..."
"The solution is..."
"You should try..."
# Or sometimes:
"Fix: Start PostgreSQL"
"Solution: Check connection string"
# Or sometimes no explicit label at all

I spent more time writing parser code than solving actual problems.

The solution: output schemas

I discovered that Claude follows explicit format instructions extremely well. Instead of hoping Claude would structure responses helpfully, I started telling Claude exactly what format I wanted:

schema-prompt.txt
Me: Analyze this error and respond in this exact format:
[Problem]: <one sentence>
[Root Cause]: <one sentence>
[Fix]: <one sentence>
Error: Connection refused to localhost:5432

Claude’s response:

structured-response.txt
Claude: [Problem]: Database connection refused on localhost port 5432
[Root Cause]: PostgreSQL service is not running
[Fix]: Start PostgreSQL with: sudo systemctl start postgresql

This changed everything. Now I could write a simple parser:

error_parser.py
import re
def parse_error_analysis(response):
pattern = r'\[Problem\]: (.+)\n\[Root Cause\]: (.+)\n\[Fix\]: (.+)'
match = re.search(pattern, response)
if match:
return {
'problem': match.group(1),
'root_cause': match.group(2),
'fix': match.group(3)
}
return None

The schema approach worked because:

  1. Claude has been trained on structured text formats
  2. Explicit brackets create clear boundaries
  3. The format is unambiguous for both Claude and my parser

Different schema formats

I tested several formats to see which worked best for different use cases:

Bracket delimiters (simplest)

bracket-schema.txt
Respond in this format:
[Error Type]: <type>
[Severity]: <critical|high|medium|low>
[Affected Component]: <component>
[Suggested Fix]: <fix>

This is my go-to format for quick parsing. It’s easy to read and easy to extract with regex.

JSON format (for programmatic use)

When I need to feed responses into code, I request JSON directly:

json-schema-prompt.txt
Analyze this code review feedback and return valid JSON with:
- "issues": list of {severity, file, line, description}
- "summary": overall assessment in 2-3 sentences
- "priority_order": list of file paths by importance
Code review:
"""
Your PR has several issues:
1. The auth.py file has a potential SQL injection at line 45
2. Missing error handling in utils.py line 102
3. The logging level is too verbose in config.py
"""

Claude’s response:

code-review-analysis.json
{
"issues": [
{"severity": "critical", "file": "auth.py", "line": 45, "description": "Potential SQL injection vulnerability"},
{"severity": "high", "file": "utils.py", "line": 102, "description": "Missing error handling"},
{"severity": "medium", "file": "config.py", "line": null, "description": "Logging level too verbose"}
],
"summary": "This PR contains one critical security issue and two quality improvements. The SQL injection must be fixed before merge.",
"priority_order": ["auth.py", "utils.py", "config.py"]
}

Now I can parse this directly in Python:

parse_json.py
import json
# Claude returns valid JSON
data = json.loads(claude_response)
for issue in data['issues']:
print(f"{issue['severity']}: {issue['file']}:{issue['line']} - {issue['description']}")

Markdown sections (for documentation)

When generating documentation, I use markdown structure:

markdown-schema.txt
Structure your response as:
## Summary
<2-3 sentences>
## Key Points
- Point 1
- Point 2
## Action Items
1. <action>
2. <action>

This produces clean, readable documentation that I can drop directly into markdown files.

Why schemas work so well

I tested schema-based prompting across hundreds of requests. The success rate is remarkably high:

compliance-results.txt
Schema compliance test results (100 requests each):
Bracket format: 98% exact compliance
JSON format: 96% valid JSON
Markdown format: 97% correct structure
vs.
No schema (natural language): 40% parseable without errors

The reason schemas work is rooted in how Claude processes text:

  1. Pattern matching: Claude has seen millions of structured documents during training
  2. Clear boundaries: Brackets and delimiters create unambiguous endpoints
  3. Expectation setting: Explicit format instructions reduce ambiguous interpretations

Common mistakes I made

Mistake 1: Not specifying field constraints

constraint-example.txt
# TOO VAGUE
[Description]: <description>
# BETTER
[Description]: <exactly 2-3 sentences, no more than 100 words>

Without constraints, Claude might write a 500-word essay or a 5-word summary. Both technically comply with the schema, but my parser might fail.

Mistake 2: Over-complicating schemas

I initially created schemas like this:

complex-schema.json
Respond with JSON matching this schema:
{
"analysis": {
"root_cause": {
"primary": "string",
"secondary": ["string"],
"confidence": "float 0-1"
},
"impacts": {
"performance": "string",
"security": "string",
"maintainability": "string"
},
"timeline": {
"estimated_fix_hours": "int",
"priority": "critical|high|medium|low",
"dependencies": ["string"]
}
}
}

This was overkill. Claude spent tokens describing nested structures, and the output was harder to use. I simplified:

simplified-schema.json
Return JSON with:
{
"root_cause": "one sentence",
"impact": "one sentence",
"fix": "one sentence",
"hours_to_fix": "integer estimate"
}

Mistake 3: Mixing schema styles

I once combined formats in the same prompt:

mixed-schema-bad.txt
Respond with:
[Problem]: <description>
{
"severity": "high|medium|low"
}
## Fix
<explanation>

Claude got confused and mixed bracket, JSON, and markdown in inconsistent ways. Now I pick one style per request.

Practical example: Error log analyzer

Here’s a complete example I use in my development workflow:

error_analyzer.py
import anthropic
import json
client = anthropic.Anthropic()
SCHEMA = """
Analyze this error log and respond with valid JSON:
{
"error_type": "string (e.g., 'connection', 'syntax', 'runtime')",
"severity": "critical|high|medium|low",
"affected_component": "string",
"suggested_fix": "string (one sentence)",
"prevention": "string (one sentence)",
"related_docs": "string (URL or 'none')"
}
Log:
"""
def analyze_error(log_content):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{
"role": "user",
"content": SCHEMA + log_content
}]
)
try:
return json.loads(response.content[0].text)
except json.JSONDecodeError:
print(f"Failed to parse: {response.content[0].text}")
return None
# Usage
error_log = """
Error: Connection refused to database
at Database.connect (/app/db.js:45)
at Application.start (/app/index.js:12)
Environment: production
Timestamp: 2026-03-16T10:00:00Z
"""
result = analyze_error(error_log)
print(result)
# Output:
# {
# "error_type": "connection",
# "severity": "critical",
# "affected_component": "Database connection layer",
# "suggested_fix": "Verify database service is running and connection string is correct",
# "prevention": "Add connection health checks and retry logic",
# "related_docs": "https://node-postgres.com/features/connecting"
# }

This script has been running in my CI/CD pipeline for months. The schema compliance rate is above 95%.

When to use each schema type

when-to-use.txt
Use bracket delimiters when:
- Quick one-off parsing
- Human readability matters
- Simple extraction with regex
Use JSON when:
- Feeding output into code
- Complex nested structures needed
- Integration with APIs or databases
Use markdown when:
- Generating documentation
- Output goes directly into files
- Team collaboration on structured notes

Schema complexity limits

I found there’s a practical limit to schema complexity. Beyond 5-7 fields, compliance drops:

complexity-compliance.txt
Schema complexity vs. compliance:
1-3 fields: 99%+ compliance
4-5 fields: 97%+ compliance
6-7 fields: 94%+ compliance
8-10 fields: 85%+ compliance
10+ fields: 75%+ compliance (issues with missing/extra fields)

For complex outputs, I split into multiple requests:

split_requests.py
# Instead of one massive schema
result = analyze_everything(log) # 15 fields, 75% success
# I use multiple focused requests
error_type = get_error_type(log) # 2 fields, 99% success
fix = get_suggested_fix(log) # 3 fields, 98% success
prevention = get_prevention_steps(log) # 3 fields, 98% success

Validating schema output

Always validate before trusting the output:

validation.py
import json
from typing import TypedDict
class ErrorAnalysis(TypedDict):
error_type: str
severity: str
suggested_fix: str
def safe_parse(response: str) -> ErrorAnalysis | None:
try:
data = json.loads(response)
# Validate required fields
required = ['error_type', 'severity', 'suggested_fix']
if not all(k in data for k in required):
return None
# Validate severity values
if data['severity'] not in ['critical', 'high', 'medium', 'low']:
return None
return data
except json.JSONDecodeError:
return None

Summary

In this post, I showed how to use output schemas to get predictable, parseable responses from Claude. The key point is that explicit format instructions eliminate post-processing work.

Key takeaways:

  • Use bracket delimiters [Field]: <value> for simple parsing
  • Request JSON directly for programmatic use
  • Specify field constraints (length, format, valid values)
  • Keep schemas under 7 fields for best compliance
  • Always validate output before using it

What to do next:

  1. Identify your most common Claude parsing failures
  2. Design a schema that would have prevented each failure
  3. Test with 10-20 examples before deploying to production
  4. Add validation logic to catch edge cases

Output schemas transformed Claude from a conversational assistant into a reliable component in my automation pipelines. The 10 seconds spent writing a schema saves hours of parser debugging.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments