DEV Community

Cover image for Rust: Deserialize JSON with Serde
Juliano Alves
Juliano Alves

Posted on • Originally published at juliano-alves.com

Rust: Deserialize JSON with Serde

JSON can be a complicated format to manipulate, even though it's well structured text. When using static type languages - like Rust - we want the almighty compiler on our side, no one wants to work with pure text... but it happens :)

The following JSON has a list of profiles:

[
  {
    "id": 1,
    "type": "personal",
    "details": {
      "firstName": "Juliano",
      "lastName": "Alves",
      "primaryAddress": 7777777
    }
  },
  {
    "id": 2,
    "type": "business",
    "details": {
      "name": "Juliano Business",
      "companyRole": "OWNER",
      "primaryAddress": 8888888
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

They are not the same though. We have to handle two problems:

  1. The field names should follow snake_case instead of camelCase;
  2. The response has different fields

Let's see how we can parse this json into a strongly typed data structure using Serde.

The Data Structure

Both profiles have id and type fields, so instead of the similarities we should think about the differences between them first. Let's define PersonalDetails and BusinessDetails:

struct PersonalDetails {
    first_name: String,
    last_name: String,
    primary_address: i32
}

struct BusinessDetails {
    name: String,
    company_role: String,
    primary_address: i32
}
Enter fullscreen mode Exit fullscreen mode

Now we have to make sure that serde will know how to use these structs.

The derive macro

Based on Rust's #[derive] mechanism, serde provides a handful macro that can be used to generate implementations of Serialize and Deserialize. We just need to deserialize, so let's add it. Once we are changing the code, we can derive Debug as well, to make it easier to print later:

#[derive(Deserialize, Debug)]
struct PersonalDetails {

#[derive(Deserialize, Debug)]
struct BusinessDetails {
Enter fullscreen mode Exit fullscreen mode

Renaming fields

In order to customize fields, serde provides container attributes. To rename camelCase we can use #[serde(rename_all = "...")]:

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct PersonalDetails {

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct BusinessDetails {
Enter fullscreen mode Exit fullscreen mode

Parsing different objects

The distinction between the profiles is given by the type attribute. When the field identifying which variant we are dealing with is inside the content, serde calls it internally tagged.

Let's define an enum Profile, with two profiles Personal and Business. Using tag we will tell serde to use the type field in order to decide between the variants:

#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
enum Profile {
    Personal {
        id: i32,
        details: PersonalDetails,
    },
    Business {
        id: i32,
        details: BusinessDetails,
    },
}
Enter fullscreen mode Exit fullscreen mode

Now we can parse the json with serde_json:

let profiles: Vec<Profile> = serde_json::from_str(data)?;
Enter fullscreen mode Exit fullscreen mode

Full example

Cargo.toml

[package]
name = "serde-example"
version = "0.1.0"
authors = ["Juliano Alves <von.juliano@gmail.com>"]
edition = "2018"

[dependencies]
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
Enter fullscreen mode Exit fullscreen mode

main.rs

#[macro_use]
extern crate serde_derive;

use serde_json::Result;

fn main() -> Result<()>  {
    let data = r#"
    [
      {
        "id": 1,
        "type": "personal",
        "details": {
          "firstName": "Juliano",
          "lastName": "Alves",
          "primaryAddress": 7777777
        }
      },
      {
        "id": 2,
        "type": "business",
        "details": {
          "name": "Juliano Business",
          "companyRole": "OWNER",
          "primaryAddress": 8888888
        }
      }
    ]
    "#;

    let profiles: Vec<Profile> = serde_json::from_str(data)?;
    println!("{:#?}", profiles);

    Ok(())
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct PersonalDetails {
    first_name: String,
    last_name: String,
    primary_address: i32
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct BusinessDetails {
    name: String,
    company_role: String,
    primary_address: i32
}

#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
enum Profile {
    Personal {
        id: i32,
        details: PersonalDetails,
    },
    Business {
        id: i32,
        details: BusinessDetails,
    },
}
Enter fullscreen mode Exit fullscreen mode

Originally posted on my blog

Latest comments (1)

Collapse
 
terkwood profile image
Felix Terkhorn

This is helpful! I've been manually renaming individual fields out of laziness - I remember attempting to use rename_all a few months ago, but must have done something wrong. Looking forward to using your example as a guide and trying again.