DEV Community

loading...

Rust - Serde Json By Example

pintuch
・6 min read

Basic struct serialization and deserialization

Serialization happens via serde_json::to_string.
serde_json::to_string_pretty can be used for enhancing the readibility if you're printing the data.

pub fn to_string<T: ?Sized>(value: &T) -> Result<String> 
where
    T: Serialize
Enter fullscreen mode Exit fullscreen mode

Deserialization happens via serde_json::from_str

pub fn from_str<'a, T>(s: &'a str) -> Result<T> 
where
    T: Deserialize<'a>
Enter fullscreen mode Exit fullscreen mode

Since, the return type for both is a Result<T>, the examples below end up unwrap()ing the values.

Unit structs

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct U;

fn main() {
    let u = U;

    // serialization
    let s = serde_json::to_string(&u).unwrap();
    // `s` represented as `null`
    println!("{}", s);

    // deserialization
    let d: U = serde_json::from_str("null").unwrap();
    // `d` is U
    println!("{:?}", d);
}
// Output:
// null
// U
Enter fullscreen mode Exit fullscreen mode

Tuple structs

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct T(u8, f64, bool, String);

fn main() {
    // serialization
    let t = T(10u8, 3.14159, true, "Hello".to_owned());
    let s = serde_json::to_string(&t).unwrap();
    // `s` represented as `[10, 3.14159, true, "Hello"]`
    println!("{}", s);

    // deserialization
    let d: T = serde_json::from_str(r#"[10, 3.14159, true, "Hello"]"#).unwrap();
    // `d` is `T(10u8, 3.14159, true, "Hello".to_owned())`;
    println!("{:?}", d);
}
// Output:
// [10,3.14159,true,"Hello"]
// T(10, 3.14159, true, "Hello")

Enter fullscreen mode Exit fullscreen mode

Haskell Newtype-like structs

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct N(i32);

fn main() {
    // serialization
    let n = N(10i32);
    let s = serde_json::to_string(&n).unwrap();
    // `s` represented as `10`
    println!("{}", s);

    // deserialization
    let d: N = serde_json::from_str("10").unwrap();
    // `d` is `N(10)`;
    println!("{:?}", d);
}
// Output:
// 10
// N(10)

Enter fullscreen mode Exit fullscreen mode

C-like structs

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct C {
    a: i32,
    b: f64,
    c: bool,
    d: String,
}

fn main() {
    // serialization
    let t = C {
        a: 10,
        b: 3.14159,
        c: true,
        d: "Hello".to_owned(),
    };
    let s = serde_json::to_string(&t).unwrap();
    // `s` represented as `{"a": 10, "b": 3.14159, "c": true, "d": "Hello"}`
    println!("{}", s);

    // deserialization
    let d: C = serde_json::from_str(&s).unwrap();
    println!("{:?}", d);
}

// Output:
// {"a":10,"b":3.14159,"c":true,"d":"Hello"}
// C { a: 10, b: 3.14159, c: true, d: "Hello" }

Enter fullscreen mode Exit fullscreen mode

Optional values

The examples you have come across so far had structs with required values.
null is a valid json value that is often used in cases data is missing.
Take for example the following:

#[derive(Serialize, Deserialize)]
struct C {
    a: i32,
    b: f64,
}

// serialization with missing value
let t = C {
  b: 3.14159,
};
serde_json::to_string(&t).unwrap();
Enter fullscreen mode Exit fullscreen mode

The above program would fail to compile since we've missed a entirely

error[E0063]: missing field `a` in initializer of `C`
Enter fullscreen mode Exit fullscreen mode

Deserialization with a null value for a would cause errors.
Its type is i32 and we can't arbitrarily choose 0 to indicate absence of data.
In such cases, it's preferred that we mark the values as Optional as shown below.

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct C {
    a: Option<i32>,
    b: f64,
}

fn main() {
    // serialization with missing value
    let t = C {
        a: None,
        b: 3.14159,
    };
    println!("{}", serde_json::to_string(&t).unwrap());
    // `{"a": null, "b": 3.14159}`

    // deserialization with missing value
    let d: C = serde_json::from_str(r#"{"a": null, "b": 3.14159}"#).unwrap();
    // `d` equals `C { a: None, b: 3.14159 }`
    println!("{:?}", d);

    println!("Hello, world!");
}

// Output:
// {"a":null,"b":3.14159}
// C { a: None, b: 3.14159 }
// Hello, world!

Enter fullscreen mode Exit fullscreen mode

Default values

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Test {
    // Use default_str_fun() as the default if "my_str" is missing
    #[serde(default = "default_str_fun")]
    my_str: String,

    // Use MyType::default if "my_type" is missing
    #[serde(default)]
    my_type: MyType,
}

fn default_str_fun() -> String {
    "hi".to_owned()
}

/// Timeout in seconds.
#[derive(Deserialize, Debug)]
struct MyType(Vec<i32>);
impl std::default::Default for MyType {
    fn default() -> Self {
        Self(vec![1, 2, 3])
    }
}

fn main() {
    let d1: Test = serde_json::from_str(r#"{"my_type": [1]}"#).unwrap();
    // `d1` equals `Test { my_str: "hi".to_owned(), my_type: MyType(vec![1]) }`
    println!("{:?}", d1);

    let d2: Test = serde_json::from_str(r#"{"my_str": "hello world"}"#).unwrap();
    // `d2` equals `Test { my_str: "hello world".to_owned(), my_type: MyType(vec![1,2,3]) }`
    println!("{:?}", d2);

    println!("Hello, world!");
}

// Output:
// Test { my_str: "hi", my_type: MyType([1]) }
// Test { my_str: "hello world", my_type: MyType([1, 2, 3]) }
// Hello, world!

Enter fullscreen mode Exit fullscreen mode

Missing field vs null value

A null value isn't the same as a missing field altogether.
The following program panics on deserialization.

// ================
// BROKEN
// ================
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Test {
    // Use default_str_fun() as the default if "my_str" is missing
    #[serde(default = "default_str_fun")]
    my_str: String,

    // Use MyType::default if "my_type" is missing
    #[serde(default)]
    my_type: MyType,
}

fn default_str_fun() -> String {
    "hi".to_owned()
}

/// Timeout in seconds.
#[derive(Deserialize, Debug)]
struct MyType(Vec<i32>);
impl std::default::Default for MyType {
    fn default() -> Self {
        Self(vec![1, 2, 3])
    }
}

fn main() {

    let d: Test = serde_json::from_str(r#"{
        "my_str": "hello world", 
        "my_type": null
    }"#).unwrap();
    println!("{:?}", d);
}

// Output:
// Running `target/debug/playground`
// thread 'main' panicked at 'called `Result::unwrap()` on an 
// `Err` value: Error("invalid type: null, expected a sequence", line: 3, column: 23)', 
// src/main.rs:32:10
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Enter fullscreen mode Exit fullscreen mode

json! macro

The json! macro allows one to construct serde_json::Value objects by hand.

let j: serde_json::Value = json!({
    "a": 10u8,
    "b": 3.14159,
    "c": true,
    "d": "Hello".to_owned(),
});
let s = j.to_string();
// `s` represented as `{"a": 10, "b": 3.14159, "c": true, "d": "Hello"}`
Enter fullscreen mode Exit fullscreen mode

Deserializing to Map

// deserializing to Map<String, Value>
use serde_json::{Value, Map};
println!(
    "{:?}", 
    serde_json::from_str::<Map<String, Value>>(r#"
        {"gender":"male","blah":25}
        "#).unwrap());
// {"blah": Number(25), "gender": String("male")}
Enter fullscreen mode Exit fullscreen mode

enums

The default enum representation has the enum variant 'externally tagging' the content.

#[derive(Serialize, Deserialize)]
enum Color {
    C { red: u8, green: u8, blue: u8 },
    T(u8, u8, u8),
    N(i32),
    D,
}
let a = Color::C { 
    red: 10,
    green: 100,
    blue: 125
}; 
// `a` represented as `{"C":{"red":10,"green":100, "blue": 125}}`

let b = Color::T(10, 100, 125); 
// `b` represented as `{"T":[10,100,125]}`

let c = Color::N(0);
// `c` represented as `{"N":0}`

let d = Color::D;
// `d` represented as `"D"`
Enter fullscreen mode Exit fullscreen mode

It's also possible to delineate the tag and content separately in the object
next to each other

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "body")]
enum Color {
    C { red: u8, green: u8, blue: u8 },
    T(u8, u8, u8),
    N(i32),
    D,
}

fn main() {
    let a = Color::C {
        red: 10,
        green: 100,
        blue: 125,
    };
    println!("{}", serde_json::to_string(&a).unwrap());
    // {"type":"C","body":{"red":10,"green":100,"blue":125}}
}

Enter fullscreen mode Exit fullscreen mode

Ommitting the content should merge the tag with the rest of the content.

// ===============
// BROKEN PROGRAM
// ===============

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Color {
    C { red: u8, green: u8, blue: u8 },
    T(u8, u8, u8),
    N(i32),
    D,
}

fn main() {
    println!("Hello, world!");
    let a = Color::C {
        red: 10,
        green: 100,
        blue: 125,
    };
    println!("{}", serde_json::to_string(&a).unwrap());
}

Enter fullscreen mode Exit fullscreen mode

Wait! The code fails to build. Let's look at the error message.

Compiling playground v0.0.1 (/playground)
error: #[serde(tag = "...")] cannot be used with tuple variants
 --> src/main.rs:7:5
  |
7 |     T(u8, u8, u8),
  |     ^^^^^^^^^^^^^

error[E0277]: the trait bound `Color: Serialize` is not satisfied
    --> src/main.rs:19:42
     |
19   |     println!("{}", serde_json::to_string(&a).unwrap());
     |                                          ^^ the trait `Serialize` is not implemented for `Color`
     | 
    ::: /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.61/src/ser.rs:2221:17
     |
2221 |     T: ?Sized + Serialize,
     |                 --------- required by this bound in `serde_json::to_string`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground`

To learn more, run the command again with --verbose.
Enter fullscreen mode Exit fullscreen mode

Removing the tuple variant makes it work again! I haven't really spent time figuring
out what's gone wrong here.

// Using just serde `tag` and no `content`

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Color {
    C { red: u8, green: u8, blue: u8 },

    // commenting out the tuple variant
    // T(u8, u8, u8),

    N(i32),
    D,
}

fn main() {
    let a = Color::C {
        red: 10,
        green: 100,
        blue: 125,
    };
    println!("{}", serde_json::to_string(&a).unwrap());
    // {"type":"C","red":10,"green":100,"blue":125}
}
Enter fullscreen mode Exit fullscreen mode

If you don't want any tag information creeping in the serialized string,
choose the untagged representation. While deserializing, serde would try
each variant and returns the first one that succeeds.

// Untagged version

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Color {
    C { red: u8, green: u8, blue: u8 },
    T(u8, u8, u8),
    N(i32),
    D,
}

fn main() {
    let a = Color::C {
        red: 10,
        green: 100,
        blue: 125,
    };
    println!("{}", serde_json::to_string(&a).unwrap());
    // {"red":10,"green":100,"blue":125}
}

Enter fullscreen mode Exit fullscreen mode

Aliasing and renaming fields

You can change the case for individual or all of the fields while serializing
to json representation.

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Address {
    id: i64,
    street_name: String,
    zip_code: String,
    city: String,
}

let address = Address {
    id: 1,
    street_name: "ABC street".to_owned(),
    zip_code: "10101010".to_owned(),
    city: "XYZ City".to_owned(),
};
let js = serde_json::to_string(&address).unwrap();
// js represented as:
// `{"id": 1, "streetName": "ABC street", "zipCode": "10101010", "city": "XYZ City"}`

Enter fullscreen mode Exit fullscreen mode

Fields can also be aliased by multiple names while deserializing the json string.
e.g. field attribute: #[serde(alias = "<Choose your field alias>")]

You can avoid serializing or deserializing a field. The attributes to consider are the following:
#[serde(skip_deserializing)], #[serde(skip_serializing)], #[serde(skip)]

#[derive(Serialize, Deserialize, Debug)]
struct XYZP {
    #[serde(alias = "gender")]
    #[serde(alias = "category")]
    sex: String,

    #[serde(rename = "blah")]
    age_years: u8,

    #[serde(skip)]
    weight_kg: u8,
}

let xyzp = XYZP {
    sex: "male".to_owned(),
    age_years: 25,
    weight_kg: 76,
};

println!("{}", serde_json::to_string(&xyzp).unwrap());
// {"sex":"male","blah":25}

println!(
    "{:?}", 
    serde_json::from_str::<XYZP>(r#"
        {"gender":"male","blah":25}
        "#
    ).unwrap());
// XYZP { sex: "male", age_years: 25, weight_kg: 0 }

Enter fullscreen mode Exit fullscreen mode

Discussion (0)