Serializing and deserializing complex structures

In some cases, you might want to serialize or deserialize complex data structures. Most of the time, you will have a crate that does this for you (such as the chrono crate for dates and times). But in some cases this is not enough. Suppose you have a data structure that has a field that can take a value of either 1 or 2, and that each of them means something different. In Rust, you would use an enumeration for it, but we may not always have control of external APIs, for example.

Let's look at this structure:

{
"timestamp": "2018-01-16T15:43:04",
"type": 1,
}

And let's say we have some code, almost ready to compile, which represents this structure:

#[derive(Debug)]
enum DateType {
FirstType,
SecondType,
}

#[derive(Debug, Serialize, Deserialize)]
struct MyDate {
timestamp: NaiveDateTime,
#[serde(rename = "type")]
date_type: DateType,
}

As you can see, we will need to define that NaiveDateTime structure. We will need to add the following to our Cargo.toml file:

[dependencies.chrono]
version = "0.4.0"
features = ["serde"]

And then add the imports at the top of the main.rs file:

extern crate chrono;
use chrono::NaiveDateTime;

The only thing left is to implement Serialize and Deserialize for DateType. But what if this enumeration is not part of our crate and we cannot modify it? We can, in this case, specify a way of making it work by using a function in our crate, adding the function name as a serde deserialize_with attribute in the MyDate type:

#[derive(Debug, Serialize, Deserialize)]
struct MyDate {
timestamp: NaiveDateTime,
#[serde(rename = "type",
deserialize_with = "deserialize_date_type")]
date_type: DateType,
}

Then, we will need to implement that function. It is required that the function has the following signature:

use serde::{Deserializer, Serializer};

fn deserialize_date_type<'de, D>(deserializer: D)
-> Result<DateType, D::Error>
where D: Deserializer<'de>
{
unimplemented!()
}

fn serialize_date_type<S>(date_type: &DateType, serializer: S)
-> Result<S::Ok, S::Error>
where S: Serializer
{
unimplemented!()
}

Then, it's as simple as using the Deserializer and Serializer traits. You can get the full API documentation by running cargo doc, but we will find out how to do it for this particular case. Let's start with the Serialize implementation, since it's simpler than the Deserialize implementation. You will just need to call the serialize_u8() (or any other integer) method with the appropriate value, as you can see in the following code snippet:

fn serialize_date_type<S>(date_type: &DateType, serializer: S)
-> Result<S::Ok, S::Error>
where S: Serializer
{
use serde::Serializer;

serializer.serialize_u8(match date_type {
DateType::FirstType => 1,
DateType::SecondType => 2,
})
}

As you can see, we just serialize an integer depending on the variant of the date type. To select which integer to serialize, we just match the enumeration. The Deserializer trait uses the visitor pattern, though, so we also need to implement a small structure that implements the Visitor trait. This is not very difficult, but can be a bit complex the first time we do it. Let's check it out:

fn deserialize_date_type<'de, D>(deserializer: D)
-> Result<DateType, D::Error>
where D: Deserializer<'de>
{
use std::fmt;
use serde::Deserializer;
use serde::de::{self, Visitor};

struct DateTypeVisitor;

impl<'de> Visitor<'de> for DateTypeVisitor {
type Value = DateType;

fn expecting(&self, formatter: &mut fmt::Formatter)
-> fmt::Result
{
formatter.write_str("an integer between 1 and 2")
}

fn visit_u64<E>(self, value: u64)
-> Result<Self::Value, E>
where E: de::Error
{
match value {
1 => Ok(DateType::FirstType),
2 => Ok(DateType::SecondType),
_ => {
let error =
format!("type out of range: {}", value);
Err(E::custom(error))
}
}
}

// Similar for other methods, if you want:
// - visit_i8
// - visit_i16
// - visit_i32
// - visit_i64
// - visit_u8
// - visit_u16
// - visit_u32
}

deserializer.deserialize_u64(DateTypeVisitor)
}

As you can see, I implemented the visit_u64() function for Visitor. This is because serde_json seems to use that function when serializing and deserializing integers. You can implement the rest if you want Visitor to be compatible with other serialization and deserialization frontends (such as XML, TOML, and others). You can see that the structure and the Visitor trait implementations are defined inside the function, so we do not pollute the namespace outside the function.

You will be able to test it with a new main() function:

fn main() {
let json = r#"{
"timestamp": "2018-01-16T15:43:04",
"type": 1
}"#;

let in_rust: MyDate = serde_json::from_str(json)
.expect("JSON parsing failed");
println!("In Rust: {:?}", in_rust);

    let back_to_json = serde_json::to_string_pretty(&in_rust)
.expect("Rust to JSON failed");
println!("In JSON: {}", back_to_json);
}

It should show the following output:

You can of course implement the Serialize and Deserialize traits for full structures and enumerations, if the serde attributes are not enough for your needs. Their implementation is close to the ones seen in these functions, but you will need to check the API for more complex data serialization and deserialization. You can find a great guide at https://serde.rs/ explaining the specific options for this crate.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.84.157