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:
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: This error indicates that your application cannot connect to the database.The connection is being refused on localhost port 5432, which is typicallyused by PostgreSQL. Let me explain what's happening...
[Three paragraphs of explanation]
To fix this, you should:1. Check if PostgreSQL is running2. Verify your connection string3. 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:
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 NoneThis worked sometimes. But when Claude changed formats, my parser broke:
# 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 allI 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:
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:5432Claude’s response:
Claude: [Problem]: Database connection refused on localhost port 5432[Root Cause]: PostgreSQL service is not running[Fix]: Start PostgreSQL with: sudo systemctl start postgresqlThis changed everything. Now I could write a simple parser:
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 NoneThe schema approach worked because:
- Claude has been trained on structured text formats
- Explicit brackets create clear boundaries
- 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)
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:
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 452. Missing error handling in utils.py line 1023. The logging level is too verbose in config.py"""Claude’s response:
{ "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:
import json
# Claude returns valid JSONdata = 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:
Structure your response as:## Summary<2-3 sentences>
## Key Points- Point 1- Point 2
## Action Items1. <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:
Schema compliance test results (100 requests each):
Bracket format: 98% exact complianceJSON format: 96% valid JSONMarkdown format: 97% correct structure
vs.
No schema (natural language): 40% parseable without errorsThe reason schemas work is rooted in how Claude processes text:
- Pattern matching: Claude has seen millions of structured documents during training
- Clear boundaries: Brackets and delimiters create unambiguous endpoints
- Expectation setting: Explicit format instructions reduce ambiguous interpretations
Common mistakes I made
Mistake 1: Not specifying field constraints
# 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:
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:
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:
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:
import anthropicimport 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
# Usageerror_log = """Error: Connection refused to database at Database.connect (/app/db.js:45) at Application.start (/app/index.js:12)
Environment: productionTimestamp: 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%.
Related knowledge
When to use each schema type
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 notesSchema complexity limits
I found there’s a practical limit to schema complexity. Beyond 5-7 fields, compliance drops:
Schema complexity vs. compliance:
1-3 fields: 99%+ compliance4-5 fields: 97%+ compliance6-7 fields: 94%+ compliance8-10 fields: 85%+ compliance10+ fields: 75%+ compliance (issues with missing/extra fields)For complex outputs, I split into multiple requests:
# Instead of one massive schemaresult = analyze_everything(log) # 15 fields, 75% success
# I use multiple focused requestserror_type = get_error_type(log) # 2 fields, 99% successfix = get_suggested_fix(log) # 3 fields, 98% successprevention = get_prevention_steps(log) # 3 fields, 98% successValidating schema output
Always validate before trusting the output:
import jsonfrom 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 NoneSummary
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:
- Identify your most common Claude parsing failures
- Design a schema that would have prevented each failure
- Test with 10-20 examples before deploying to production
- 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:
- 👨💻 Reddit Discussion: Claude Power User Tips
- 👨💻 Anthropic: Structured Outputs
- 👨💻 Claude Prompt Engineering Guide
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments