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 point
  • string: Unicode characters
  • boolean: true or false
  • array: ordered list of JSON values
  • object: a collection of key-value pairs, where key is a string and the value is a JSON value
  • null: 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:

  1. derive Deserialize for the struct
  2. 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…