Skip to main content

lattice_protocol/
jsonrpc.rs

1//! JSON-RPC 2.0 message types -- the wire shape of every LSP
2//! exchange. Transport-agnostic: these types serialize through
3//! `serde_json::to_string`, the codec writes the bytes.
4//!
5//! Also the wire shape of the Claude Code IDE peer (`lattice-claude-code`)
6//! and the MCP server in `lattice-ai`; none of them owns the types, so none
7//! of them depends on another to share them. Framing (LSP's `Content-Length`
8//! headers, a WebSocket frame) is each peer's codec's business.
9//!
10//! # Examples
11//!
12//! ```
13//! use lattice_protocol::{Message, Request, RequestId, Response, ResponseError};
14//! use serde_json::json;
15//!
16//! // Outgoing: build, then serialize for the codec.
17//! let req = Request::new(RequestId::from_u64(1), "initialize", Some(json!({})));
18//! let bytes = Message::Request(req).to_json().unwrap();
19//! assert!(bytes.starts_with(br#"{"jsonrpc":"2.0","id":1,"method":"initialize""#));
20//!
21//! // Incoming: decode by shape — `id` + `result` is a response.
22//! let wire = br#"{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}"#;
23//! let Message::Response(resp) = Message::from_json(wire).unwrap() else {
24//!     panic!("expected a response");
25//! };
26//! assert_eq!(resp.id, RequestId::Number(1));
27//! assert!(resp.error.is_none());
28//!
29//! // `method` without `id` is a notification.
30//! let note = br#"{"jsonrpc":"2.0","method":"$/progress","params":{}}"#;
31//! assert!(matches!(Message::from_json(note), Ok(Message::Notification(_))));
32//!
33//! // Answering a server-initiated request we cannot serve.
34//! let reply = Response::err(
35//!     RequestId::String("cfg-1".into()),
36//!     ResponseError {
37//!         code: lattice_protocol::jsonrpc::error_codes::METHOD_NOT_FOUND,
38//!         message: "unsupported".into(),
39//!         data: None,
40//!     },
41//! );
42//! let json = String::from_utf8(Message::Response(reply).to_json().unwrap()).unwrap();
43//! assert!(json.contains(r#""id":"cfg-1""#) && !json.contains("result"));
44//! ```
45//!
46//! ## Why we keep `params` / `result` as `serde_json::Value`
47//!
48//! `lsp-types` has typed structs for every method. We could
49//! parameterize on `<P, R>` but that would push the typing into
50//! every actor and complicate dynamic dispatch (one mailbox
51//! handling 30+ method shapes). Instead the codec yields
52//! `serde_json::Value`-bearing messages and the actor downcasts
53//! per method via `serde_json::from_value::<T>(...)`. That
54//! confines the type discipline to the message-handling layer
55//! where it belongs.
56//!
57//! ## Id correlation
58//!
59//! Every outgoing request gets a fresh [`RequestId::Number`] from
60//! the actor's monotonic counter. Incoming responses are matched
61//! against an id → oneshot map; matching is a `HashMap` lookup.
62//! Server-initiated requests (e.g. `workspace/configuration`) use
63//! their own id; we echo it back on the response unchanged.
64
65use serde::{Deserialize, Serialize};
66use serde_json::Value;
67
68/// JSON-RPC 2.0 protocol version literal. Always exactly this
69/// string on the wire; we preserve it as-is so a non-conforming
70/// server can be flagged early.
71pub const JSONRPC_VERSION: &str = "2.0";
72
73/// Per JSON-RPC, an id is a string, a number, or null. LSP
74/// servers in the wild send all three; clients (us) only emit
75/// `Number`, but we accept all on the read path.
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(untagged)]
78pub enum RequestId {
79    /// A numeric id — what lattice itself always sends.
80    Number(i64),
81    /// A string id, as some servers use for their own requests.
82    String(String),
83    /// Some LSP servers send `null` for cancellation acks. The
84    /// JSON-RPC spec discourages it but doesn't forbid it.
85    Null,
86}
87
88impl RequestId {
89    /// Construct a `Number` id; the common case for our actor's
90    /// outgoing requests.
91    ///
92    /// Values above `i64::MAX` wrap (an `as` cast); a monotonic counter
93    /// never gets there.
94    pub fn from_u64(n: u64) -> Self {
95        // i64 fits any sane request count; the actor counter is
96        // monotonic from 0 and we'll never overflow in practice.
97        // i64::MAX is ~9.2 quintillion.
98        RequestId::Number(n as i64)
99    }
100}
101
102/// One JSON-RPC request that expects a response. `params` is
103/// optional per JSON-RPC; LSP methods that take no parameters
104/// should send `params: null` (or omit). We always include it as
105/// `null` to keep the wire layout uniform.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Request {
108    /// Protocol version; [`JSONRPC_VERSION`] on everything we build. Not
109    /// validated on decode.
110    pub jsonrpc: String,
111    /// Correlates the eventual [`Response`].
112    pub id: RequestId,
113    /// Method name, e.g. `textDocument/hover`.
114    pub method: String,
115    /// Method-specific parameters. The actor downcasts this with
116    /// `serde_json::from_value::<lsp_types::FooParams>(...)`.
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub params: Option<Value>,
119}
120
121impl Request {
122    /// Build an outgoing request. `id` is supplied by the actor
123    /// from its monotonic counter; pairs the response back.
124    pub fn new(id: RequestId, method: impl Into<String>, params: Option<Value>) -> Self {
125        Self {
126            jsonrpc: JSONRPC_VERSION.to_string(),
127            id,
128            method: method.into(),
129            params,
130        }
131    }
132}
133
134/// One JSON-RPC notification: fire-and-forget, no response. LSP
135/// uses these for `textDocument/didChange`,
136/// `textDocument/publishDiagnostics`, `$/progress`, and the like.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Notification {
139    /// Protocol version; [`JSONRPC_VERSION`] on everything we build.
140    pub jsonrpc: String,
141    /// Method name, e.g. `textDocument/didChange`.
142    pub method: String,
143    /// Method-specific parameters; omitted from the wire when `None`.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub params: Option<Value>,
146}
147
148impl Notification {
149    /// Build an outgoing notification.
150    pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
151        Self {
152            jsonrpc: JSONRPC_VERSION.to_string(),
153            method: method.into(),
154            params,
155        }
156    }
157}
158
159/// One JSON-RPC response. Either `result` or `error` is set;
160/// never both.
161///
162/// Decoding does not enforce the "never both" half: a message carrying both
163/// decodes with both set, and consumers check `error` first. A message with
164/// an `id` but *neither* key is not a response at all —
165/// [`Message::from_json`] rejects it as [`MessageDecodeError::Malformed`].
166///
167/// A successful result of JSON `null` (e.g. `shutdown`'s) deserializes to
168/// `result: None`, indistinguishable from an absent key.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct Response {
171    /// Protocol version; [`JSONRPC_VERSION`] on everything we build.
172    pub jsonrpc: String,
173    /// The id of the [`Request`] this answers, echoed unchanged.
174    pub id: RequestId,
175    /// The success payload.
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub result: Option<Value>,
178    /// The failure payload.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub error: Option<ResponseError>,
181}
182
183impl Response {
184    /// Successful response constructor.
185    pub fn ok(id: RequestId, result: Value) -> Self {
186        Self {
187            jsonrpc: JSONRPC_VERSION.to_string(),
188            id,
189            result: Some(result),
190            error: None,
191        }
192    }
193
194    /// Error-response constructor.
195    pub fn err(id: RequestId, error: ResponseError) -> Self {
196        Self {
197            jsonrpc: JSONRPC_VERSION.to_string(),
198            id,
199            result: None,
200            error: Some(error),
201        }
202    }
203}
204
205/// JSON-RPC error envelope. `code` is one of the standard
206/// integers from the JSON-RPC spec ([`error_codes`]), a JSON-RPC
207/// implementation-defined server error (`-32099 ..= -32000`), or one of
208/// LSP's own codes (`-32899 ..= -32800`); `message` is
209/// human-readable; `data` is whatever the server attaches.
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
211pub struct ResponseError {
212    /// Numeric error code; see [`error_codes`].
213    pub code: i64,
214    /// Human-readable description.
215    pub message: String,
216    /// Optional structured detail, as the sender chose.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub data: Option<Value>,
219}
220
221/// Standard JSON-RPC 2.0 error codes plus the LSP-defined ones.
222/// Used by the actor to build error responses to server-initiated
223/// requests we can't fulfil.
224pub mod error_codes {
225    /// Invalid JSON received by the server.
226    pub const PARSE_ERROR: i64 = -32700;
227    /// JSON sent is not a valid Request object.
228    pub const INVALID_REQUEST: i64 = -32600;
229    /// The method does not exist / is not available.
230    pub const METHOD_NOT_FOUND: i64 = -32601;
231    /// Invalid method parameter(s).
232    pub const INVALID_PARAMS: i64 = -32602;
233    /// Internal JSON-RPC error.
234    pub const INTERNAL_ERROR: i64 = -32603;
235
236    // LSP-defined codes. `SERVER_NOT_INITIALIZED` sits in JSON-RPC's
237    // implementation-defined -32099..=-32000 range; the rest in LSP's own
238    // reserved -32899..=-32800 range.
239    /// A request arrived before the `initialize` handshake completed.
240    pub const SERVER_NOT_INITIALIZED: i64 = -32002;
241    /// The request was well-formed and understood, but failed (LSP 3.17).
242    pub const REQUEST_FAILED: i64 = -32803;
243    /// The request was cancelled (`$/cancelRequest`) and the server stopped
244    /// work on it.
245    pub const REQUEST_CANCELLED: i64 = -32800;
246    /// The document changed while the request was in flight, so the result
247    /// would be stale; the client should re-request if still relevant.
248    pub const CONTENT_MODIFIED: i64 = -32801;
249}
250
251/// One incoming or outgoing JSON-RPC message. The codec yields
252/// `Message` from the wire; the actor matches on the variant.
253///
254/// We tag variants by the presence of `id` and `method`:
255/// - request: `id` + `method`
256/// - response: `id` + (`result` xor `error`), no `method`
257/// - notification: `method`, no `id`
258///
259/// The custom deserialize (rather than `#[serde(untagged)]`)
260/// avoids an O(n²) try-each-variant pattern for every incoming
261/// blob and gives precise error messages.
262#[derive(Debug, Clone)]
263pub enum Message {
264    /// Has `id` and `method`: expects a [`Response`].
265    Request(Request),
266    /// Has `id` and `result` / `error`, no `method`.
267    Response(Response),
268    /// Has `method`, no `id`: fire-and-forget.
269    Notification(Notification),
270}
271
272impl Message {
273    /// Decode one JSON-RPC message from a UTF-8 JSON byte slice.
274    /// Returns the typed variant; structurally invalid input
275    /// (missing both id and method, etc.) returns
276    /// [`MessageDecodeError::Malformed`].
277    pub fn from_json(bytes: &[u8]) -> Result<Self, MessageDecodeError> {
278        // Parse to Value first so we can branch on shape without
279        // allocating three different typed parses on failure.
280        let v: Value = serde_json::from_slice(bytes).map_err(MessageDecodeError::Json)?;
281        let obj = v
282            .as_object()
283            .ok_or_else(|| MessageDecodeError::Malformed("top-level not an object".into()))?;
284        let has_method = obj.contains_key("method");
285        let has_id = obj.contains_key("id");
286        let has_result = obj.contains_key("result");
287        let has_error = obj.contains_key("error");
288
289        if has_method && has_id {
290            let req: Request = serde_json::from_value(v).map_err(MessageDecodeError::Json)?;
291            Ok(Message::Request(req))
292        } else if has_method {
293            let n: Notification = serde_json::from_value(v).map_err(MessageDecodeError::Json)?;
294            Ok(Message::Notification(n))
295        } else if has_id && (has_result || has_error) {
296            let r: Response = serde_json::from_value(v).map_err(MessageDecodeError::Json)?;
297            Ok(Message::Response(r))
298        } else {
299            Err(MessageDecodeError::Malformed(
300                "neither method nor result/error present".into(),
301            ))
302        }
303    }
304
305    /// Serialize this message to JSON bytes ready for the
306    /// codec. Uses compact (no-indent) JSON; LSP servers are
307    /// agnostic and we save a few bytes per message.
308    pub fn to_json(&self) -> Result<Vec<u8>, serde_json::Error> {
309        match self {
310            Message::Request(r) => serde_json::to_vec(r),
311            Message::Response(r) => serde_json::to_vec(r),
312            Message::Notification(n) => serde_json::to_vec(n),
313        }
314    }
315}
316
317/// Decode failures from [`Message::from_json`].
318#[derive(Debug, thiserror::Error)]
319pub enum MessageDecodeError {
320    /// `serde_json` couldn't parse the bytes as JSON.
321    #[error("invalid JSON: {0}")]
322    Json(#[from] serde_json::Error),
323    /// JSON parsed, but the object didn't match
324    /// request/response/notification shapes.
325    #[error("malformed JSON-RPC message: {0}")]
326    Malformed(String),
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use serde_json::json;
333
334    #[test]
335    fn request_round_trip() {
336        let req = Request::new(
337            RequestId::from_u64(1),
338            "textDocument/hover",
339            Some(
340                json!({"textDocument": {"uri": "file:///x"}, "position": {"line": 0, "character": 0}}),
341            ),
342        );
343        let bytes = serde_json::to_vec(&req).unwrap();
344        let parsed = Message::from_json(&bytes).unwrap();
345        match parsed {
346            Message::Request(r) => {
347                assert_eq!(r.method, "textDocument/hover");
348                assert_eq!(r.id, RequestId::Number(1));
349                assert!(r.params.is_some());
350            }
351            _ => panic!("expected Request"),
352        }
353    }
354
355    #[test]
356    fn notification_round_trip() {
357        let n = Notification::new("initialized", Some(json!({})));
358        let bytes = serde_json::to_vec(&n).unwrap();
359        let parsed = Message::from_json(&bytes).unwrap();
360        assert!(matches!(parsed, Message::Notification(_)));
361    }
362
363    #[test]
364    fn response_ok_round_trip() {
365        let r = Response::ok(RequestId::from_u64(7), json!({"capabilities": {}}));
366        let bytes = serde_json::to_vec(&r).unwrap();
367        let parsed = Message::from_json(&bytes).unwrap();
368        match parsed {
369            Message::Response(resp) => {
370                assert_eq!(resp.id, RequestId::Number(7));
371                assert!(resp.result.is_some());
372                assert!(resp.error.is_none());
373            }
374            _ => panic!("expected Response"),
375        }
376    }
377
378    #[test]
379    fn response_err_round_trip() {
380        let r = Response::err(
381            RequestId::from_u64(7),
382            ResponseError {
383                code: error_codes::METHOD_NOT_FOUND,
384                message: "no such method".into(),
385                data: None,
386            },
387        );
388        let bytes = serde_json::to_vec(&r).unwrap();
389        let parsed = Message::from_json(&bytes).unwrap();
390        match parsed {
391            Message::Response(resp) => {
392                assert!(resp.result.is_none());
393                let e = resp.error.unwrap();
394                assert_eq!(e.code, error_codes::METHOD_NOT_FOUND);
395            }
396            _ => panic!("expected Response"),
397        }
398    }
399
400    #[test]
401    fn string_request_id_round_trips() {
402        // LSP servers MAY use string ids; we MUST preserve them
403        // verbatim on the response we send back.
404        let raw =
405            br#"{"jsonrpc":"2.0","id":"abc-123","method":"workspace/configuration","params":{}}"#;
406        let parsed = Message::from_json(raw).unwrap();
407        match parsed {
408            Message::Request(r) => assert_eq!(r.id, RequestId::String("abc-123".into())),
409            _ => panic!("expected Request"),
410        }
411    }
412
413    #[test]
414    fn null_request_id_round_trips() {
415        // Some servers send null id on cancellation acks. Accept
416        // it, don't crash.
417        let raw = br#"{"jsonrpc":"2.0","id":null,"result":null}"#;
418        let parsed = Message::from_json(raw).unwrap();
419        match parsed {
420            Message::Response(r) => assert_eq!(r.id, RequestId::Null),
421            _ => panic!("expected Response"),
422        }
423    }
424
425    #[test]
426    fn malformed_json_is_error() {
427        let err = Message::from_json(b"{ not json").unwrap_err();
428        assert!(matches!(err, MessageDecodeError::Json(_)));
429    }
430
431    #[test]
432    fn message_without_method_or_result_is_malformed() {
433        // Has id but neither method nor result/error.
434        let err = Message::from_json(br#"{"jsonrpc":"2.0","id":1}"#).unwrap_err();
435        assert!(matches!(err, MessageDecodeError::Malformed(_)));
436    }
437
438    #[test]
439    fn top_level_not_object_is_malformed() {
440        let err = Message::from_json(b"42").unwrap_err();
441        assert!(matches!(err, MessageDecodeError::Malformed(_)));
442    }
443
444    #[test]
445    fn omitted_params_round_trips_as_none() {
446        // Some servers omit `params` for parameterless methods
447        // like `shutdown`.
448        let raw = br#"{"jsonrpc":"2.0","id":99,"method":"shutdown"}"#;
449        let parsed = Message::from_json(raw).unwrap();
450        match parsed {
451            Message::Request(r) => assert!(r.params.is_none()),
452            _ => panic!("expected Request"),
453        }
454    }
455}