Skip to main content

a2a_protocol_types/
jsonrpc.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! JSON-RPC 2.0 envelope types.
5//!
6//! A2A 0.3.0 uses JSON-RPC 2.0 as its wire protocol. This module provides the
7//! request/response envelope types. Protocol-method-specific parameter and
8//! result types live in [`crate::params`] and the individual domain modules.
9//!
10//! # Key types
11//!
12//! - [`JsonRpcRequest`] — outbound method call.
13//! - [`JsonRpcResponse`] — inbound response (success **or** error, untagged union).
14//! - [`JsonRpcError`] — structured error object carried in error responses.
15//! - [`JsonRpcVersion`] — newtype that always serializes/deserializes as `"2.0"`.
16
17use std::fmt;
18
19use serde::{Deserialize, Deserializer, Serialize, Serializer};
20
21// ── JsonRpcVersion ────────────────────────────────────────────────────────────
22
23/// The JSON-RPC protocol version marker.
24///
25/// Always serializes as the string `"2.0"`. Deserialization rejects any value
26/// other than `"2.0"`.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct JsonRpcVersion;
29
30impl Default for JsonRpcVersion {
31    fn default() -> Self {
32        Self
33    }
34}
35
36impl fmt::Display for JsonRpcVersion {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.write_str("2.0")
39    }
40}
41
42impl Serialize for JsonRpcVersion {
43    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
44        serializer.serialize_str("2.0")
45    }
46}
47
48impl<'de> Deserialize<'de> for JsonRpcVersion {
49    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
50        let s = String::deserialize(deserializer)?;
51        if s == "2.0" {
52            Ok(Self)
53        } else {
54            Err(serde::de::Error::custom(format!(
55                "expected JSON-RPC version \"2.0\", got \"{s}\""
56            )))
57        }
58    }
59}
60
61// ── JsonRpcId ─────────────────────────────────────────────────────────────────
62
63/// A JSON-RPC 2.0 request/response identifier.
64///
65/// Per spec, valid values are a string, a number, or `null`. When the field is
66/// absent entirely (notifications), represent as `None`.
67pub type JsonRpcId = Option<serde_json::Value>;
68
69// ── JsonRpcRequest ────────────────────────────────────────────────────────────
70
71/// A JSON-RPC 2.0 request object.
72///
73/// When `id` is `None`, the request is a *notification* and no response is
74/// expected.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct JsonRpcRequest {
77    /// Protocol version — always `"2.0"`.
78    pub jsonrpc: JsonRpcVersion,
79
80    /// Request identifier; `None` for notifications.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub id: JsonRpcId,
83
84    /// A2A method name (e.g. `"message/send"`).
85    pub method: String,
86
87    /// Method-specific parameters.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub params: Option<serde_json::Value>,
90}
91
92impl JsonRpcRequest {
93    /// Creates a new request with the given `id` and `method`.
94    #[must_use]
95    pub fn new(id: serde_json::Value, method: impl Into<String>) -> Self {
96        Self {
97            jsonrpc: JsonRpcVersion,
98            id: Some(id),
99            method: method.into(),
100            params: None,
101        }
102    }
103
104    /// Creates a new request with `params`.
105    #[must_use]
106    pub fn with_params(
107        id: serde_json::Value,
108        method: impl Into<String>,
109        params: serde_json::Value,
110    ) -> Self {
111        Self {
112            jsonrpc: JsonRpcVersion,
113            id: Some(id),
114            method: method.into(),
115            params: Some(params),
116        }
117    }
118
119    /// Creates a notification (no `id`, no response expected).
120    #[must_use]
121    pub fn notification(method: impl Into<String>, params: Option<serde_json::Value>) -> Self {
122        Self {
123            jsonrpc: JsonRpcVersion,
124            id: None,
125            method: method.into(),
126            params,
127        }
128    }
129}
130
131// ── JsonRpcResponse ───────────────────────────────────────────────────────────
132
133/// A JSON-RPC 2.0 response: either a success with a `result` or an error with
134/// an `error` object.
135///
136/// The `untagged` representation tries `Success` first; if `result` is absent
137/// it falls back to `Error`.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(untagged)]
140pub enum JsonRpcResponse<T> {
141    /// Successful response carrying a typed result.
142    Success(JsonRpcSuccessResponse<T>),
143    /// Error response carrying a structured error object.
144    Error(JsonRpcErrorResponse),
145}
146
147// ── JsonRpcSuccessResponse ────────────────────────────────────────────────────
148
149/// A successful JSON-RPC 2.0 response.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct JsonRpcSuccessResponse<T> {
152    /// Protocol version — always `"2.0"`.
153    pub jsonrpc: JsonRpcVersion,
154
155    /// Matches the `id` of the corresponding request.
156    pub id: JsonRpcId,
157
158    /// The method result.
159    pub result: T,
160}
161
162impl<T> JsonRpcSuccessResponse<T> {
163    /// Creates a success response for the given request `id`.
164    #[must_use]
165    pub const fn new(id: JsonRpcId, result: T) -> Self {
166        Self {
167            jsonrpc: JsonRpcVersion,
168            id,
169            result,
170        }
171    }
172}
173
174// ── JsonRpcErrorResponse ──────────────────────────────────────────────────────
175
176/// An error JSON-RPC 2.0 response.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct JsonRpcErrorResponse {
179    /// Protocol version — always `"2.0"`.
180    pub jsonrpc: JsonRpcVersion,
181
182    /// Matches the `id` of the corresponding request, or `null` if the id
183    /// could not be determined.
184    pub id: JsonRpcId,
185
186    /// Structured error object.
187    pub error: JsonRpcError,
188}
189
190impl JsonRpcErrorResponse {
191    /// Creates an error response for the given request `id`.
192    #[must_use]
193    pub const fn new(id: JsonRpcId, error: JsonRpcError) -> Self {
194        Self {
195            jsonrpc: JsonRpcVersion,
196            id,
197            error,
198        }
199    }
200}
201
202// ── JsonRpcError ──────────────────────────────────────────────────────────────
203
204/// The error object within a JSON-RPC 2.0 error response.
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct JsonRpcError {
207    /// Numeric error code.
208    pub code: i32,
209
210    /// Short human-readable error message.
211    pub message: String,
212
213    /// Optional additional error details.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub data: Option<serde_json::Value>,
216}
217
218impl JsonRpcError {
219    /// Creates a new error object.
220    #[must_use]
221    pub fn new(code: i32, message: impl Into<String>) -> Self {
222        Self {
223            code,
224            message: message.into(),
225            data: None,
226        }
227    }
228
229    /// Creates a new error object with additional data.
230    #[must_use]
231    pub fn with_data(code: i32, message: impl Into<String>, data: serde_json::Value) -> Self {
232        Self {
233            code,
234            message: message.into(),
235            data: Some(data),
236        }
237    }
238}
239
240impl fmt::Display for JsonRpcError {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        write!(f, "[{}] {}", self.code, self.message)
243    }
244}
245
246impl std::error::Error for JsonRpcError {}
247
248// ── Tests ─────────────────────────────────────────────────────────────────────
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn version_serializes_as_2_0() {
256        let v = JsonRpcVersion;
257        let s = serde_json::to_string(&v).expect("serialize");
258        assert_eq!(s, "\"2.0\"");
259    }
260
261    #[test]
262    fn version_rejects_wrong_version() {
263        let result: Result<JsonRpcVersion, _> = serde_json::from_str("\"1.0\"");
264        assert!(result.is_err(), "should reject non-2.0 version");
265    }
266
267    #[test]
268    fn version_accepts_2_0() {
269        let v: JsonRpcVersion = serde_json::from_str("\"2.0\"").expect("deserialize");
270        assert_eq!(v, JsonRpcVersion);
271    }
272
273    #[test]
274    fn request_roundtrip() {
275        let req = JsonRpcRequest::with_params(
276            serde_json::json!(1),
277            "message/send",
278            serde_json::json!({"message": {}}),
279        );
280        let json = serde_json::to_string(&req).expect("serialize");
281        assert!(json.contains("\"jsonrpc\":\"2.0\""));
282        assert!(json.contains("\"method\":\"message/send\""));
283
284        let back: JsonRpcRequest = serde_json::from_str(&json).expect("deserialize");
285        assert_eq!(back.method, "message/send");
286    }
287
288    #[test]
289    fn success_response_roundtrip() {
290        let resp: JsonRpcResponse<serde_json::Value> =
291            JsonRpcResponse::Success(JsonRpcSuccessResponse::new(
292                Some(serde_json::json!(42)),
293                serde_json::json!({"status": "ok"}),
294            ));
295        let json = serde_json::to_string(&resp).expect("serialize");
296        assert!(json.contains("\"result\""));
297        assert!(!json.contains("\"error\""));
298    }
299
300    #[test]
301    fn error_response_roundtrip() {
302        let resp: JsonRpcResponse<serde_json::Value> =
303            JsonRpcResponse::Error(JsonRpcErrorResponse::new(
304                Some(serde_json::json!(1)),
305                JsonRpcError::new(-32601, "Method not found"),
306            ));
307        let json = serde_json::to_string(&resp).expect("serialize");
308        assert!(json.contains("\"error\""));
309        assert!(json.contains("-32601"));
310    }
311
312    #[test]
313    fn notification_has_no_id() {
314        let n = JsonRpcRequest::notification("task/cancel", None);
315        let json = serde_json::to_string(&n).expect("serialize");
316        assert!(
317            !json.contains("\"id\""),
318            "notification must omit id: {json}"
319        );
320    }
321}