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
}
}
]
They are not the same though. We have to handle two problems:
- The field names should follow
snake_case
instead ofcamelCase
; - The response has different fields
Let’s see how we can parse this json into an 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
}
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 {
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 {
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,
},
}
Now we can parse the json with serde_json
:
let profiles: Vec<Profile> = serde_json::from_str(data)?;
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"
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,
},
}
Comments