Briefly about Serde in Rust

In this article, we'll look at the basics of Serde in Rust.

Let's install

First, let's add Serde to the project. In the file Cargo.toml add the following lines to the section [dependencies]:

serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Here we connect not only the Serde library itself, but also serde_json, which will allow you to work with the JSON format. The functionality of Serde is expanded through various adapters, so that depending on needs, other modules can be connected, such as serde_yaml or serde_toml.

After adding dependencies, run the command cargo build to download and compile Serde along with the project.

Basics of working with Serde

Serialization is the process of converting Rust data structures into a format that can be easily transferred or stored. Deserialization is the reverse process, converting data from a format back into Rust data structures.

To start working with Serde, add the attribute #[derive(Serialize, Deserialize)] to data structures. This allows Serde to automatically generate code to serialize and deserialize these structures.

Example of serializing and deserializing a structure in JSON:

use serde::{Serialize, Deserialize};
use serde_json::{to_string, from_str};

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

fn main() {
    // создание экземпляра структуры User
    let user = User {
        id: 1,
        name: "Ivan Otus".to_string(),
        email: "ivan.otus@example.com".to_string(),
    };

    // сериализация структуры User в JSON
    let json = to_string(&user).unwrap();
    println!("Serialized JSON: {}", json);

    // десериализация JSON обратно в структуру User
    let deserialized_user: User = from_str(&json).unwrap();
    println!("Deserialized User: {:?}", deserialized_user);
}

Serde has various annotations and attributes to customize the serialization and deserialization process. Most used:

  • #[serde(rename = "new_name")]: Renames a field when serializing or deserializing.

  • #[serde(default)]: Uses the default value for the field if it is missing during deserialization.

  • #[serde(skip_serializing)]: Omits the field when serializing.

  • #[serde(skip_deserializing)]: Skips the field when deserializing.

  • #[serde(with = "module")]: Uses the specified module to serialize and deserialize the field.

Let's try to use everything at once:

use serde::{Deserialize, Serialize};
use serde_with::serde_as;

#[serde_as]
#[derive(Serialize, Deserialize)]
struct User {
    #[serde(rename = "userId")]
    id: u32,
    #[serde(default = "default_name")]
    name: String,
    #[serde(skip_serializing)]
    password: String,
    #[serde(skip_deserializing)]
    secret: String,
    #[serde(with = "serde_with::rust::display_fromstr")]
    age: u32,
}

fn default_name() -> String {
    "Unknown".to_string()
}

fn main() {
    let user = User {
        id: 1,
        name: "Ivan Otus".to_string(),
        password: "secret".to_string(),
        secret: "hidden".to_string(),
        age: 30,
    };

    let serialized = serde_json::to_string(&user).unwrap();
    println!("Serialized: {}", serialized);

    let deserialized: User = serde_json::from_str(&serialized).unwrap();
    println!("Deserialized: {:?}", deserialized);
}

#[serde(rename = "userId")] renaming the field id V userId during serialization and deserialization.

#[serde(default = "default_name")] uses the function default_name to set a default value for a field nameif it is missing during deserialization.

#[serde(skip_serializing)] skips the field password when serialized, so it will not be included in the JSON string.

#[serde(skip_deserializing)] skips field secret when deserializing, so its value will remain unchanged after deserialization.

#[serde(with = "serde_with::rust::display_fromstr")] uses module serde_with::rust::display_fromstr to serialize and deserialize a field age. Suitable for fields with custom types that implement traits Display And FromStr.

Custom serializers and deserializers

In some cases, the serialization and deserialization process may need to be more finely tuned than standard Serde mechanisms allow. In such cases, you can use custom serializers and deserializers.

Custom serializer is a function or structure that implements a trait Serializer from Serde. Similarly, a custom deserializer implements the trait Deserializer.

Example of a custom serializer for serialization Option<String> in JSON as an empty string if the value None:

use serde::{Serialize, Serializer};

fn serialize_option_string<S>(value: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match value {
        Some(v) => serializer.serialize_str(v),
        None => serializer.serialize_str(""),
    }
}

#[derive(Serialize)]
struct MyStruct {
    #[serde(serialize_with = "serialize_option_string")]
    name: Option<String>,
}

fn main() {
    let my_struct = MyStruct {
        name: None,
    };

    let json = serde_json::to_string(&my_struct).unwrap();
    println!("Serialized JSON: {}", json);
}

Let's say there is a structure Eventand you need to serialize it into JSON, where the date will be presented in the format “yyyy-mm-dd”:

use serde::{Serialize, Serializer};
use chrono::{DateTime, Utc, NaiveDate};

struct Event {
    name: String,
    date: DateTime<Utc>,
}

fn serialize_date<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let formatted_date = date.format("%Y-%m-%d").to_string();
    serializer.serialize_str(&formatted_date)
}

impl Serialize for Event {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("Event", 2)?;
        state.serialize_field("name", &self.name)?;
        state.serialize_field("date", &serialize_date(&self.date, serializer)?)?;
        state.end()
    }
}

fn main() {
    let event = Event {
        name: "RustConf".to_string(),
        date: DateTime::from_utc(NaiveDate::from_ymd(2022, 9, 12).and_hms(0, 0, 0), Utc),
    };

    let serialized = serde_json::to_string(&event).unwrap();
    println!("Serialized: {}", serialized);
}

Similarly, you can create a custom deserializer that will convert a string in the format “yyyy-mm-dd” back to DateTime<Utc>:

use serde::{Deserialize, Deserializer};
use chrono::{DateTime, Utc, NaiveDate};
use serde::de::{self, Visitor};
use std::fmt;

struct Event {
    name: String,
    date: DateTime<Utc>,
}

struct DateTimeVisitor;

impl<'de> Visitor<'de> for DateTimeVisitor {
    type Value = DateTime<Utc>;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("a string in the format YYYY-MM-DD")
    }

    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        NaiveDate::parse_from_str(value, "%Y-%m-%d")
            .map(|date| DateTime::from_utc(date.and_hms(0, 0, 0), Utc))
            .map_err(de::Error::custom)
    }
}

fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_str(DateTimeVisitor)
}

#[derive(Deserialize)]
struct Event {
    name: String,
    #[serde(deserialize_with = "deserialize_date")]
    date: DateTime<Utc>,
}

fn main() {
    let data = r#"{"name": "RustConf", "date": "2022-09-12"}"#;
    let event: Event = serde_json::from_str(data).unwrap();
    println!("Deserialized: {:?}", event);
}

Integration with other libs

Serde can be used in Rocket for serialization and deserialization of data transmitted in HTTP requests and responses.

An example of using Serde with Rocket to create a simple REST API:

#[macro_use] extern crate rocket;

use rocket::serde::{json::Json, Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Task {
    id: u32,
    description: String,
}

#[post("/tasks", format = "json", data = "<task>")]
fn create_task(task: Json<Task>) -> Json<Task> {
    task
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![create_task])
}

Defining the structure Task, which will be used to serialize and deserialize task data. create an endpoint /taskswhich takes JSON data and returns it back to the client.

Tokyo is an asynchronous runtime for Rust that allows you to write high-performance asynchronous applications. An example of using Serde with Tokio to asynchronously read and write JSON data:

use tokio::fs::File;
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
}

async fn read_user_from_file(file_path: &str) -> io::Result<User> {
    let mut file = File::open(file_path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    let user: User = serde_json::from_str(&contents)?;
    Ok(user)
}

async fn write_user_to_file(user: &User, file_path: &str) -> io::Result<()> {
    let mut file = File::create(file_path).await?;
    let contents = serde_json::to_string(user)?;
    file.write_all(contents.as_bytes()).await?;
    Ok(())
}

#[tokio::main]
async fn main() -> io::Result<()> {
    let user = User {
        id: 1,
        name: "Ivan Otus".to_string(),
    };

    write_user_to_file(&user, "user.json").await?;
    let read_user = read_user_from_file("user.json").await?;
    println!("Read user: {:?}", read_user);

    Ok(())
}

Defining the structure User and two asynchronous functions: read_user_from_file to read user from JSON file and write_user_to_file to write the user to a JSON file. Using Serde to serialize and deserialize a structure User.

More details from Serde read the documentation.


My colleagues from OTUS talk about popular programming languages ​​and practical tools as part of online courses. By link you can view the full catalog of courses and also sign up for open lessons.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *