Skip to main content

claude_code/
message_parser.rs

1//! JSON message parser for the Claude Code CLI protocol.
2//!
3//! This module converts raw JSON values from the CLI output stream into
4//! typed [`Message`] variants. It handles all message types: user, assistant,
5//! system, result, and stream_event.
6//!
7//! The primary entry point is [`parse_message()`].
8
9use serde_json::Value;
10
11use crate::errors::MessageParseError;
12use crate::types::{
13    AssistantMessage, ContentBlock, Message, ResultMessage, StreamEvent, SystemMessage, TextBlock,
14    ThinkingBlock, ToolResultBlock, ToolUseBlock, UserContent, UserMessage,
15};
16
17/// Returns a human-readable type name for a JSON value (for error messages).
18fn value_type_name(value: &Value) -> &'static str {
19    match value {
20        Value::Null => "null",
21        Value::Bool(_) => "bool",
22        Value::Number(_) => "number",
23        Value::String(_) => "str",
24        Value::Array(_) => "list",
25        Value::Object(_) => "dict",
26    }
27}
28
29/// Extracts a required field from a JSON object, returning a descriptive error if missing.
30fn get_required<'a>(
31    data: &'a serde_json::Map<String, Value>,
32    key: &str,
33    context: &str,
34    full_data: &Value,
35) -> std::result::Result<&'a Value, MessageParseError> {
36    data.get(key).ok_or_else(|| {
37        MessageParseError::new(
38            format!("Missing required field in {context} message: '{key}'"),
39            Some(full_data.clone()),
40        )
41    })
42}
43
44/// Parses an array of JSON content blocks into typed [`ContentBlock`] variants.
45///
46/// Supports `text`, `thinking`, `tool_use`, and `tool_result` block types.
47/// Unknown block types are silently skipped.
48fn parse_content_blocks(blocks: &[Value], include_thinking: bool) -> Vec<ContentBlock> {
49    let mut content_blocks = Vec::new();
50
51    for block in blocks {
52        let Some(block_obj) = block.as_object() else {
53            continue;
54        };
55
56        let Some(block_type) = block_obj.get("type").and_then(Value::as_str) else {
57            continue;
58        };
59
60        match block_type {
61            "text" => {
62                if let Some(text) = block_obj.get("text").and_then(Value::as_str) {
63                    content_blocks.push(ContentBlock::Text(TextBlock {
64                        text: text.to_string(),
65                    }));
66                }
67            }
68            "thinking" if include_thinking => {
69                if let (Some(thinking), Some(signature)) = (
70                    block_obj.get("thinking").and_then(Value::as_str),
71                    block_obj.get("signature").and_then(Value::as_str),
72                ) {
73                    content_blocks.push(ContentBlock::Thinking(ThinkingBlock {
74                        thinking: thinking.to_string(),
75                        signature: signature.to_string(),
76                    }));
77                }
78            }
79            "tool_use" => {
80                if let (Some(id), Some(name), Some(input)) = (
81                    block_obj.get("id").and_then(Value::as_str),
82                    block_obj.get("name").and_then(Value::as_str),
83                    block_obj.get("input"),
84                ) {
85                    content_blocks.push(ContentBlock::ToolUse(ToolUseBlock {
86                        id: id.to_string(),
87                        name: name.to_string(),
88                        input: input.clone(),
89                    }));
90                }
91            }
92            "tool_result" => {
93                if let Some(tool_use_id) = block_obj.get("tool_use_id").and_then(Value::as_str) {
94                    content_blocks.push(ContentBlock::ToolResult(ToolResultBlock {
95                        tool_use_id: tool_use_id.to_string(),
96                        content: block_obj.get("content").cloned(),
97                        is_error: block_obj.get("is_error").and_then(Value::as_bool),
98                    }));
99                }
100            }
101            _ => {}
102        }
103    }
104
105    content_blocks
106}
107
108/// Parses a raw JSON value from the CLI into a typed [`Message`].
109///
110/// # Arguments
111///
112/// * `data` — A JSON value representing a single message from the CLI output stream.
113///
114/// # Returns
115///
116/// - `Ok(Some(message))` — Successfully parsed into a known message type.
117/// - `Ok(None)` — The message type is unrecognized (silently skipped).
118/// - `Err(MessageParseError)` — The message is malformed or missing required fields.
119///
120/// # Supported message types
121///
122/// | `type` field | Parsed into |
123/// |-------------|-------------|
124/// | `"user"` | [`Message::User`] |
125/// | `"assistant"` | [`Message::Assistant`] |
126/// | `"system"` | [`Message::System`] |
127/// | `"result"` | [`Message::Result`] |
128/// | `"stream_event"` | [`Message::StreamEvent`] |
129///
130/// # Example
131///
132/// ```rust
133/// use claude_code::{parse_message, Message};
134/// use serde_json::json;
135///
136/// let raw = json!({
137///     "type": "system",
138///     "subtype": "initialized"
139/// });
140///
141/// let parsed = parse_message(&raw).unwrap();
142/// assert!(matches!(parsed, Some(Message::System(_))));
143/// ```
144pub fn parse_message(data: &Value) -> std::result::Result<Option<Message>, MessageParseError> {
145    let Some(obj) = data.as_object() else {
146        return Err(MessageParseError::new(
147            format!(
148                "Invalid message data type (expected dict, got {})",
149                value_type_name(data)
150            ),
151            Some(data.clone()),
152        ));
153    };
154
155    let Some(message_type) = obj.get("type").and_then(Value::as_str) else {
156        return Err(MessageParseError::new(
157            "Message missing 'type' field",
158            Some(data.clone()),
159        ));
160    };
161
162    match message_type {
163        "user" => {
164            let message = get_required(obj, "message", "user", data)?;
165            let message_obj = message.as_object().ok_or_else(|| {
166                MessageParseError::new(
167                    "Missing required field in user message: 'message'",
168                    Some(data.clone()),
169                )
170            })?;
171            let content = get_required(message_obj, "content", "user", data)?;
172
173            let user_content = if let Some(content_blocks) = content.as_array() {
174                UserContent::Blocks(parse_content_blocks(content_blocks, false))
175            } else if let Some(content_text) = content.as_str() {
176                UserContent::Text(content_text.to_string())
177            } else {
178                UserContent::Text(content.to_string())
179            };
180
181            Ok(Some(Message::User(UserMessage {
182                content: user_content,
183                uuid: obj
184                    .get("uuid")
185                    .and_then(Value::as_str)
186                    .map(ToString::to_string),
187                parent_tool_use_id: obj
188                    .get("parent_tool_use_id")
189                    .and_then(Value::as_str)
190                    .map(ToString::to_string),
191                tool_use_result: obj.get("tool_use_result").cloned(),
192            })))
193        }
194        "assistant" => {
195            let message = get_required(obj, "message", "assistant", data)?;
196            let message_obj = message.as_object().ok_or_else(|| {
197                MessageParseError::new(
198                    "Missing required field in assistant message: 'message'",
199                    Some(data.clone()),
200                )
201            })?;
202
203            let content = get_required(message_obj, "content", "assistant", data)?;
204            let model = get_required(message_obj, "model", "assistant", data)?
205                .as_str()
206                .ok_or_else(|| {
207                    MessageParseError::new(
208                        "Missing required field in assistant message: 'model'",
209                        Some(data.clone()),
210                    )
211                })?;
212
213            let blocks = content.as_array().ok_or_else(|| {
214                MessageParseError::new(
215                    "Missing required field in assistant message: 'content'",
216                    Some(data.clone()),
217                )
218            })?;
219
220            Ok(Some(Message::Assistant(AssistantMessage {
221                content: parse_content_blocks(blocks, true),
222                model: model.to_string(),
223                parent_tool_use_id: obj
224                    .get("parent_tool_use_id")
225                    .and_then(Value::as_str)
226                    .map(ToString::to_string),
227                error: obj
228                    .get("error")
229                    .and_then(Value::as_str)
230                    .map(ToString::to_string),
231            })))
232        }
233        "system" => {
234            let subtype = get_required(obj, "subtype", "system", data)?
235                .as_str()
236                .ok_or_else(|| {
237                    MessageParseError::new(
238                        "Missing required field in system message: 'subtype'",
239                        Some(data.clone()),
240                    )
241                })?;
242
243            Ok(Some(Message::System(SystemMessage {
244                subtype: subtype.to_string(),
245                data: data.clone(),
246            })))
247        }
248        "result" => {
249            let subtype = get_required(obj, "subtype", "result", data)?
250                .as_str()
251                .ok_or_else(|| {
252                    MessageParseError::new(
253                        "Missing required field in result message: 'subtype'",
254                        Some(data.clone()),
255                    )
256                })?;
257            let duration_ms = get_required(obj, "duration_ms", "result", data)?
258                .as_i64()
259                .ok_or_else(|| {
260                    MessageParseError::new(
261                        "Missing required field in result message: 'duration_ms'",
262                        Some(data.clone()),
263                    )
264                })?;
265            let duration_api_ms = get_required(obj, "duration_api_ms", "result", data)?
266                .as_i64()
267                .ok_or_else(|| {
268                    MessageParseError::new(
269                        "Missing required field in result message: 'duration_api_ms'",
270                        Some(data.clone()),
271                    )
272                })?;
273            let is_error = get_required(obj, "is_error", "result", data)?
274                .as_bool()
275                .ok_or_else(|| {
276                    MessageParseError::new(
277                        "Missing required field in result message: 'is_error'",
278                        Some(data.clone()),
279                    )
280                })?;
281            let num_turns = get_required(obj, "num_turns", "result", data)?
282                .as_i64()
283                .ok_or_else(|| {
284                    MessageParseError::new(
285                        "Missing required field in result message: 'num_turns'",
286                        Some(data.clone()),
287                    )
288                })?;
289            let session_id = get_required(obj, "session_id", "result", data)?
290                .as_str()
291                .ok_or_else(|| {
292                    MessageParseError::new(
293                        "Missing required field in result message: 'session_id'",
294                        Some(data.clone()),
295                    )
296                })?;
297
298            Ok(Some(Message::Result(ResultMessage {
299                subtype: subtype.to_string(),
300                duration_ms,
301                duration_api_ms,
302                is_error,
303                num_turns,
304                session_id: session_id.to_string(),
305                stop_reason: obj
306                    .get("stop_reason")
307                    .and_then(Value::as_str)
308                    .map(ToString::to_string),
309                total_cost_usd: obj.get("total_cost_usd").and_then(Value::as_f64),
310                usage: obj.get("usage").cloned(),
311                result: obj
312                    .get("result")
313                    .and_then(Value::as_str)
314                    .map(ToString::to_string),
315                structured_output: obj.get("structured_output").cloned(),
316            })))
317        }
318        "stream_event" => {
319            let uuid = get_required(obj, "uuid", "stream_event", data)?
320                .as_str()
321                .ok_or_else(|| {
322                    MessageParseError::new(
323                        "Missing required field in stream_event message: 'uuid'",
324                        Some(data.clone()),
325                    )
326                })?;
327            let session_id = get_required(obj, "session_id", "stream_event", data)?
328                .as_str()
329                .ok_or_else(|| {
330                    MessageParseError::new(
331                        "Missing required field in stream_event message: 'session_id'",
332                        Some(data.clone()),
333                    )
334                })?;
335            let event = get_required(obj, "event", "stream_event", data)?;
336
337            Ok(Some(Message::StreamEvent(StreamEvent {
338                uuid: uuid.to_string(),
339                session_id: session_id.to_string(),
340                event: event.clone(),
341                parent_tool_use_id: obj
342                    .get("parent_tool_use_id")
343                    .and_then(Value::as_str)
344                    .map(ToString::to_string),
345            })))
346        }
347        _ => Ok(None),
348    }
349}