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}