Parse API Responses

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 50 of 200

What Parsing a Response Means

The SDK already converts the API's JSON into typed Python objects. So what is left to parse?

In a real application, a Message object is rarely the final form of the data. You need to:

  • extract the parts your application uses: text, structured data, tool calls, and metadata,
  • convert responses into plain data, to store in a database, return from a web API, or write to logs,
  • rebuild SDK objects from stored data, and
  • map API data into your application's own types, so the rest of your code does not depend on SDK details.

This lesson brings together the techniques from the Responses section into a clean parsing layer.

API JSON ──► SDK Message object ──► your application's data
                 │                        (dataclasses, dicts, DB rows, API responses)
                 ▼
        to_dict() / to_json()  ──► storage ──► Message.model_validate() ──► SDK object again

Level 1: SDK Objects

Responses are Pydantic models: typed objects with attributes (Lesson 14).

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Name two Python web frameworks."}],
)

message.id                    # str
message.stop_reason           # str
message.usage.output_tokens   # int
message.content               # list of block objects

Attribute access, type hints, and editor autocomplete make SDK objects pleasant to work with inside your code. They are not suitable for storing or transmitting, which needs plain data.


Level 2: Plain Data with to_dict() and to_json()

The SDK provides conversion helpers on every response object:

data = message.to_dict()        # dict with nested dicts and lists
text = message.to_json()        # JSON string
MethodReturnsUse for
to_dict()dictStoring in JSON database fields, returning from web APIs, comparing in tests
to_json()strWriting to files or logs, sending as text

These methods convert nested objects too, including content blocks, usage, and citations, so you do not need to copy fields by hand.

Because responses are Pydantic models, Pydantic's own model_dump() and model_dump_json() also work. to_dict() and to_json() are the SDK's recommended helpers.


Level 3: Rebuilding SDK Objects from Stored Data

When you load a stored response, you can turn the dictionary back into a typed Message with Pydantic's model_validate():

import json

from anthropic.types import Message

stored = json.loads(saved_json_text)
message = Message.model_validate(stored)

print(message.content[0].type)   # Typed block objects again

Why rebuild: code written for live responses, such as helpers that read block.type and block.text, works unchanged on stored responses. That is useful for re-processing old responses, debugging, and tests.

Note: model_validate() is the Pydantic v2 API. Rebuilding is optional. The field_of() helper from Lesson 42 works on both objects and dictionaries without converting.


Level 4: Extracting Application Data

Most applications need only a few values from a response. Extract them in one place:

What you needHow to extract itLesson
The answer textJoin text blocks by type15, 41
Structured datamessages.parse() → parsed_output36
Tagged sectionsRegular expressions on the text37
Tool callstool_use blocks42
Statusstop_reason, stop_details20, 43
Cost and sizeusage30, 44, 45
Traceabilityid, _request_id29, 49

Level 5: Mapping to Your Own Types

The most maintainable design converts each response into an application-level object that contains exactly what your application needs, using your own names:

from dataclasses import dataclass


@dataclass
class ChatResult:
    text: str
    complete: bool
    stop_reason: str
    input_tokens: int
    output_tokens: int
    message_id: str
    request_id: str | None

Why map to your own types:

BenefitExplanation
IsolationOnly one function knows the SDK's response structure. If the SDK changes (Lesson 5), you update one place.
ClarityThe rest of the code reads result.complete instead of re-implementing stop-reason logic everywhere.
Easy serializationA small dataclass converts cleanly to JSON for your own API.
Easy testingBusiness logic can be tested with simple ChatResult objects, with no SDK objects needed.

This pattern is called an adapter: it adapts an external format, the SDK's, to your internal format.


Practical Example: A Response Parsing Layer

This module parses responses into a ChatResult, serializes it for a web API, stores the full raw response, and rebuilds it later. It includes a round-trip test that proves stored responses can be restored.

"""response_parsing.py: parse, serialize, store, and rebuild Claude responses."""

import json
from dataclasses import asdict, dataclass
from pathlib import Path

from anthropic import Anthropic
from anthropic.types import Message

COMPLETE_REASONS = {"end_turn", "stop_sequence"}
client = Anthropic()


@dataclass
class ChatResult:
    text: str
    complete: bool
    stop_reason: str
    input_tokens: int
    output_tokens: int
    message_id: str
    request_id: str | None

    def to_api_dict(self) -> dict:
        """The public shape returned by your own web API: only what the client needs."""
        return {"reply": self.text, "complete": self.complete, "id": self.message_id}


def parse_message(message: Message) -> ChatResult:
    """The single place that knows the SDK's response structure."""
    text = "".join(block.text for block in message.content if block.type == "text").strip()
    return ChatResult(
        text=text,
        complete=message.stop_reason in COMPLETE_REASONS and bool(text),
        stop_reason=message.stop_reason,
        input_tokens=message.usage.input_tokens,
        output_tokens=message.usage.output_tokens,
        message_id=message.id,
        request_id=getattr(message, "_request_id", None),
    )


def store_raw(message: Message, path: Path) -> None:
    path.write_text(message.to_json(), encoding="utf-8")


def load_raw(path: Path) -> Message:
    return Message.model_validate(json.loads(path.read_text(encoding="utf-8")))


if __name__ == "__main__":
    message = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Name two Python web frameworks in one sentence."}],
    )

    result = parse_message(message)
    print("Internal result:", asdict(result))
    print("API response:  ", json.dumps(result.to_api_dict()))

    path = Path("response.json")
    store_raw(message, path)
    restored = load_raw(path)

    restored_result = parse_message(restored)
    assert restored_result.text == result.text
    assert restored_result.message_id == result.message_id
    print("Round trip OK: the stored response parses to the same result.")

Example output:

Internal result: {'text': 'Django and Flask are two popular Python web frameworks.', 'complete': True, 'stop_reason': 'end_turn', 'input_tokens': 18, 'output_tokens': 74, 'message_id': 'msg_01Q8Faay6S7QPTvEUUQARt7h', 'request_id': 'req_018EeWyXxfu5pfWkrYcMdjWG'}
API response:   {"reply": "Django and Flask are two popular Python web frameworks.", "complete": true, "id": "msg_01Q8Faay6S7QPTvEUUQARt7h"}
Round trip OK: the stored response parses to the same result.

How It Works

  1. parse_message() is the adapter: the only function that reads the SDK's Message structure. It extracts the text by block type, decides whether the answer is complete (a normal stop reason and non-empty text), and copies the usage and IDs.
  2. ChatResult is the application's own type. Business logic, templates, and tests use it, and never depend on SDK classes.
  3. to_api_dict() defines the public response of your web API separately from the internal result. Internal details such as token counts and request IDs are not sent to browsers, which keeps the public API small and avoids leaking operational data.
  4. store_raw() saves the full original response with to_json(). Keeping the raw response, alongside your parsed data, lets you re-parse it later if your parsing logic changes or a bug is found.
  5. load_raw() rebuilds a typed Message with model_validate(), so parse_message() works on stored data exactly as on live data.
  6. The round-trip assertions prove that storing and restoring preserves what matters. Tests like this catch serialization problems early.
  7. getattr(message, "_request_id", None): the request ID belongs to the live HTTP response, so a message rebuilt from storage may not have it. getattr with a default handles both cases.

Common Mistakes

Parsing the Same Response in Many Places

What happens: different parts of the code extract text or decide completeness differently, and an SDK change requires edits everywhere.

Fix: put all response parsing in one adapter function.

Storing str(message)

What happens: the stored text is a Python representation, not JSON, and cannot be parsed back reliably.

Fix: use to_json() or to_dict().

Copying Fields by Hand for Storage

What happens: nested data such as content blocks, citations, and thinking signatures is lost or stored inconsistently.

Fix: store the complete response with the SDK's conversion helpers, and extract summaries separately.

Returning Raw SDK Responses from Your Own API

What happens: clients depend on the SDK's internal structure, and internal data is exposed to browsers.

Fix: return a small, stable, application-defined shape.

Assuming Restored Messages Have Request IDs

What happens: AttributeError or None where code expects a request ID.

Fix: store the request ID explicitly with your parsed data, and read it defensively.


Best Practices

  • Build one parsing layer (adapter) that converts SDK responses into your application's types.
  • Store raw responses with to_json() or to_dict(), alongside the parsed data.
  • Rebuild typed objects with Message.model_validate() when re-processing stored responses.
  • Define a separate public shape for responses your own API returns.
  • Decide completeness in one place, combining the stop reason with content checks.
  • Test the round trip: store, load, parse, and compare.

Exercise

  1. Extend ChatResult with a tool_calls field (a list of tool names), and fill it in parse_message().
  2. Write a Django or FastAPI view that calls Claude and returns result.to_api_dict() as JSON.
  3. Store five responses as JSON files, then write a script that loads them all with load_raw() and prints each one's stop_reason and output_tokens.
  4. Explain why to_api_dict() does not include input_tokens, output_tokens, or request_id, and when you might include some of them.
Show answers for steps 1 and 4

Step 1: add tool_calls: list[str] to ChatResult (as the last field, or with a default such as field(default_factory=list)), and in parse_message():

tool_calls=[block.name for block in message.content if block.type == "tool_use"],

Step 4: the public API should expose only what the client needs to display the reply. Token counts and request IDs are operational details. They are useful in logs and dashboards, but exposing them to every browser adds noise to the public API, and reveals internal information. You might include a request ID in an error response, so a user can quote it in a support request, or token usage in an internal admin API.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Parse API Responses and get answers drawn from it.

Signed-in readers only.