Parse API Responses
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
| Method | Returns | Use for |
|---|---|---|
to_dict() | dict | Storing in JSON database fields, returning from web APIs, comparing in tests |
to_json() | str | Writing 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. Thefield_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 need | How to extract it | Lesson |
|---|---|---|
| The answer text | Join text blocks by type | 15, 41 |
| Structured data | messages.parse() → parsed_output | 36 |
| Tagged sections | Regular expressions on the text | 37 |
| Tool calls | tool_use blocks | 42 |
| Status | stop_reason, stop_details | 20, 43 |
| Cost and size | usage | 30, 44, 45 |
| Traceability | id, _request_id | 29, 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:
| Benefit | Explanation |
|---|---|
| Isolation | Only one function knows the SDK's response structure. If the SDK changes (Lesson 5), you update one place. |
| Clarity | The rest of the code reads result.complete instead of re-implementing stop-reason logic everywhere. |
| Easy serialization | A small dataclass converts cleanly to JSON for your own API. |
| Easy testing | Business 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
parse_message()is the adapter: the only function that reads the SDK'sMessagestructure. 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.ChatResultis the application's own type. Business logic, templates, and tests use it, and never depend on SDK classes.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.store_raw()saves the full original response withto_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.load_raw()rebuilds a typedMessagewithmodel_validate(), soparse_message()works on stored data exactly as on live data.- The round-trip assertions prove that storing and restoring preserves what matters. Tests like this catch serialization problems early.
getattr(message, "_request_id", None): the request ID belongs to the live HTTP response, so a message rebuilt from storage may not have it.getattrwith 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()orto_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
- Extend
ChatResultwith atool_callsfield (a list of tool names), and fill it inparse_message(). - Write a Django or FastAPI view that calls Claude and returns
result.to_api_dict()as JSON. - Store five responses as JSON files, then write a script that loads them all with
load_raw()and prints each one'sstop_reasonandoutput_tokens. - Explain why
to_api_dict()does not includeinput_tokens,output_tokens, orrequest_id, and when you might include some of them.
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.