Rust — working with JSON Link to heading
JSON is a ubiquitous cross-platform cross-language human-readable notation. It fits perfectly with dynamically typed languages like Javascript or Python, but not so well with statically-typed languages, like C++ or Rust. Today, let’s explore how to read from JSON, save to a Rust struct, and then write back to JSON format.
Let’s start with a simple but real-use case of JSON object
{
"id": 0,
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"capabilities": {
"textDocument": {
"documentSymbol": {
"symbolKind": {
"valueSet": [
1,
2,
3,
]
}
}
}
},
"processId": null,
"rootUri": null,
"trace": "verbose",
"workspaceFolders": [
{
"name": "root",
"uri": "file:///path/to/projec/root"
}
]
}
}
Above is a real JSON message that a Language Server Protocol (LSP) client would send to the server for initialization request. A JSON value can be one of the following
number: integer or floating pointstring: Unicode charactersboolean: true or falsearray: ordered list of JSON valuesobject: a collection of key-value pairs, where key is a string and the value is a JSON valuenull: an empty value
Does enum ring any bell? In Rust, we can represent an arbitrary JSON value as a nested enum as below from serde_json crate:
enum Value {
Null,
Bool(bool),
Number(Number),
String(String),
Array(Vec<Value>),
Object(Map<String, Value>),
}
And here is where the magic happens. If the JSON value’s format is known, we can import a JSON value into a Rust struct!
JSON to struct Link to heading
We need both serde and serde_json crates. Let’s start with just WorkspaceFolder struct in the example above. Here is how we would define our struct
{
"name": string,
"uri": string
}
To import JSON into the struct, we need two simple and easy steps:
- derive
Deserializefor the struct - import using
serde_json::from_str()function
Here is the final code
use serde_json::{Result, from_str};
use serde::Deserialize;
#[derive(Deserialize)]
struct WorkspaceFolder {
name: String,
uri: String,
}
fn main() -> Result<()> {
let json_str = r#"
{
"name": "root",
"uri": "file:///path/to/projec/root"
}"#;
// Parse the JSON string into a WorkspaceFolder instance
let workspace_folder: WorkspaceFolder = from_str(json_str)?;
// Now you can use the parsed data
println!("Name: {}", workspace_folder.name);
println!("URI: {}", workspace_folder.uri);
Ok(())
}
That’s it! If there is field name or type mismatch, the result from from_str() will be an Err variant.
struct to JSON Link to heading
Now, let’s do the reverse. That is, we will export the struct into a JSON notation automatically. The idea is basically the same, but instead of Deserialize, we need Serialize and instead of serde_json::from_json(), we need serde_json::to_string() or better yet serde_json::to_string_pretty() — playground
use serde_json::{Result, to_string_pretty};
use serde::Serialize;
#[derive(Serialize)]
struct WorkspaceFolder {
name: String,
uri: String,
}
fn main() -> Result<()> {
let workspace_folder = WorkspaceFolder {
name: String::from("root"),
uri: String::from("file:///path/to/projec/root"),
};
// export the struct into JSON
let json_string = to_string_pretty(&workspace_folder)?;
println!("{}", json_string);
Ok(())
}
Bonus Link to heading
This is all great, but the examples were too trivial. There are actually more tricks available. A good reference is lsp-types crate in Rust, which defines all the JSON objects from LSP with various tricks into structs. For example here is how InitializeParams struct is defined
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
/// The process Id of the parent process that started
/// the server. Is null if the process has not been started by another process.
/// If the parent process is not alive then the server should exit (see exit notification) its process.
pub process_id: Option<u32>,
/// The rootPath of the workspace. Is null
/// if no folder is open.
#[serde(skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use `root_uri` instead when possible")]
pub root_path: Option<String>,
/// The rootUri of the workspace. Is null if no
/// folder is open. If both `rootPath` and `rootUri` are set
/// `rootUri` wins.
#[serde(default)]
#[deprecated(note = "Use `workspace_folders` instead when possible")]
pub root_uri: Option<Uri>,
/// User provided initialization options.
#[serde(skip_serializing_if = "Option::is_none")]
pub initialization_options: Option<Value>,
/// The capabilities provided by the client (editor or tool)
pub capabilities: ClientCapabilities,
/// The initial trace setting. If omitted trace is disabled ('off').
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub trace: Option<TraceValue>,
/// The workspace folders configured in the client when the server starts.
/// This property is only available if the client supports workspace folders.
/// It can be `null` if the client supports workspace folders but none are
/// configured.
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_folders: Option<Vec<WorkspaceFolder>>,
/// Information about the client.
#[serde(skip_serializing_if = "Option::is_none")]
pub client_info: Option<ClientInfo>,
/// The locale the client is currently showing the user interface
/// in. This must not necessarily be the locale of the operating
/// system.
///
/// Uses IETF language tags as the value's syntax
/// (See <https://en.wikipedia.org/wiki/IETF_language_tag>)
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
/// The LSP server may report about initialization progress to the client
/// by using the following work done token if it was passed by the client.
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
}
References Link to heading
GitHub - serde-rs/json: Strongly typed JSON library for Rust Strongly typed JSON library for Rust. Contribute to serde-rs/json development by creating an account on GitHub.
GitHub - serde-rs/serde: Serialization framework for Rust Serialization framework for Rust. Contribute to serde-rs/serde development by creating an account…
Specification This document describes the 3.17.x version of the language server protocol. An implementation for node of the 3.17.x…
GitHub - gluon-lang/lsp-types: Types for communicating with a language server *Types for communicating with a language server. Contribute to gluon-lang/lsp-types development by creating an account…