Rust First Impressions: JSON Deserialisation With Serde

For reasons that I touch upon at the end of the post I’ve been starting to look into Rust for “back-end” projects that I write in my spare time, and I want to brain dump my initial thoughts when trying something new or investigating a new crate. First up is Serde.

My first project in Rust involved me calling a HTTP API that returns JSON and doing something with the result. In Java, that I use in $DAY_JOB, parsing JSON data and mapping it to a Java POJO is usually done using the Jackson library.

The Rust equivalent of Jackson appears to be Serde, a framework for serialisation and deserialisation in Rust. While the underlying mechanisms (macros vs. annotations) are very different, the resulting code looks surprisingly similar to what I am used to. For example, the documentation for the projects endpoint of the BitBucket Server API provide the following example response;

  
  {
    "size": 1,
    "limit": 25,
    "isLastPage": true,
    "values": [
      {
        "key": "PRJ",
        "id": 1,
        "name": "My Cool Project",
        "description": "The description for my cool project.",
        "public": true,
        "type": "NORMAL",
        "links": {
          "self": [
            {
              "href": "http://link/to/project"
            }
          ]
        }
      }
    ],
    "start": 0
  }
  

My resulting Rust structs for modelling this look like the following;

  
  #[derive(Deserialize, Debug)]
  #[serde(rename_all = "camelCase")]
  struct ProjectList {
    size: u16,
    limit: u16,
    is_last_page: bool,
    next_page_start: Option<u16>,
    values: Vec<ProjectListItem>,
  }
  
  #[derive(Deserialize, Debug)]
  #[serde(rename_all = "camelCase")]
  struct ProjectListItem {
    key: String,
    id: u16,
    name: String,
    description: Option<String>,
    public: bool,
    #[serde(rename = "type")]
    project_type: String,
  }
  

This looks surprisingly similar to how the equivalent code would have looked in Java, annotated with Project Lombok and Jackson annotations, with the notable exceptions of;

  1. the convention of snake case names instead of camel case;
  2. I had to rename the type field as it is a reserved keyword, in Java I would have experienced a similar problem with the public field;
  3. All fields that don’t have the Option type are required and cannot anywhere in my application be set to an absent value (null in Java).

This last point is one of the features of Rust that interests me most. While Java has an Optional type in the standard library, it lacks meaningful equals and hashCode implementations, significantly hampering it’s usefulness. Libraries such as Vavr provide a greatly improved Option type but must be used consistenly throughout an application to provide true benefit and still do nothing to guarantee that the non-Option fields are not null. Rust comes with all of this safety out of the box, and it’s idiomatic to use it. Gone are the days of the NullPointerException (unless you explicitly activate the unsafe footgun).