Привет, Хабр!

Serde — это высокопроизводительная библиотека для сериализации и десериализации данных в Rust. Она поддерживает различные форматы данных, включая JSON, YAML, TOML, BSON и многие другие.

В этой статье рассмотрим основы Serde в Rust.

Установим

Для начала добавим Serde в проект. В файлике Cargo.toml добавляем следующие строки в раздел [dependencies]:

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

Здесь подключаем не только саму библиотеку Serde, но и serde_json, которая даст вохможность работать с JSON-форматом. Функциональность Serde расширяется через различные адаптеры, так что в зависимости от нужд можно подключить и другие модули, такие как serde_yaml или serde_toml.

После добавления зависимостей запускаем команду cargo build для загрузки и компиляции Serde вместе с проектом.

Основы работы с Serde

Сериализация — это процесс преобразования структур данных Rust в формат, который можно легко передавать или хранить. Десериализация — это обратный процесс, преобразование данных из формата обратно в структуры данных Rust.

Для старта работы с Serde добавляем атрибут #[derive(Serialize, Deserialize)] к структурам данных. Это позволяет Serde автоматически генерировать код для сериализации и десериализации этих структур.

Пример сериализации и десериализации структуры в 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 имеет различные аннотации и атрибуты для настройки процесса сериализации и десериализации. Самые используемые:

  • #[serde(rename = "new_name")]: переименовывает поле при сериализации или десериализации.

  • #[serde(default)]: использует значение по умолчанию для поля, если оно отсутствует при десериализации.

  • #[serde(skip_serializing)]: пропускает поле при сериализации.

  • #[serde(skip_deserializing)]: пропускает поле при десериализации.

  • #[serde(with = "module")]: использует указанный модуль для сериализации и десериализации поля.

Попробуем использовать все сразу:

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")] ренеймит поле id в userId при сериализации и десериализации.

#[serde(default = "default_name")] юзает функцию default_name для установки значения по умолчанию для поля name, если оно отсутствует при десериализации.

#[serde(skip_serializing)] скипает поле password при сериализации, так что оно не будет включено в JSON-строку.

#[serde(skip_deserializing)] пропускает поле secret при десериализации, так что его значение останется неизменным после десериализации.

#[serde(with = "serde_with::rust::display_fromstr")] использует модуль serde_with::rust::display_fromstr для сериализации и десериализации поля age. Годно для полей с пользовательскими типами, которые реализуют трейты Display и FromStr.

Кастомные сериализаторы и десериализаторы

В некоторых случаях может потребоваться более тонкая настройка процесса сериализации и десериализации, чем это позволяют стандартные механизмы Serde. В таких случаях можно использовать кастомные сериализаторы и десериализаторы.

Кастомный сериализатор — это функция или структура, реализующая трейт Serializer из Serde. Аналогично, кастомный десериализатор реализует трейт Deserializer.

Пример кастомного сериализатора для сериализации Option<String> в JSON как пустую строку, если значение 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);
}

Допустим, есть структура Event, и нужно сериализовать её в JSON, где дата будет представлена в формате "гггг-мм-дд":

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);
}

Аналогично, можно создать кастомный десериализатор, который будет преобразовывать строку в формате "гггг-мм-дд" обратно в 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);
}

Интеграция с другими либами

Serde может быть использована в Rocket для сериализации и десериализации данных, передаваемых в HTTP-запросах и ответах.

Пример использования Serde с Rocket для создания простого 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])
}

Определяем структуру Task, которая будет использоваться для сериализации и десериализации данных задач. создаем эндпойнт /tasks, который принимает JSON-данные и возвращает их обратно клиенту.

Tokio — это асинхронный рантайм для Rust, который позволяет писать высокопроизводительные асинхронные приложения. Пример использования Serde с Tokio для асинхронного чтения и записи JSON-данных:

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(())
}

Определяем структуру User и две асинхронные функции: read_user_from_file для чтения пользователя из JSON-файла и write_user_to_file для записи пользователя в JSON-файл. Юзаем Serde для сериализации и десериализации структуры User.

Подробней с Serde можно ознакомиться в документации.


Про востребованные языки программирования и практические инструменты мои коллеги из OTUS рассказывают в рамках онлайн-курсов. По ссылке вы можете ознакомиться с полным каталогом курсов, а также записаться на открытые уроки.

Комментарии (4)


  1. sdramare
    20.04.2024 16:41
    +4

    Это не статья, а выборочный пересказ документации. Дали бы тогда просто ссылку на https://serde.rs/ и все.


  1. Mingun
    20.04.2024 16:41
    +9

    К сожалению, с развитием serde все очень плохо. Ошибки в дизайне не правятся и не собираются. Вместо следования semver (что вообще-то является обязательным для проектов cargo, т.к. именно так cargo обрабатывает поле version), автор наперекор всем делает ломающие изменения в патчах со словами "ну, замел под ковер, значит не ломающее". На справедливую критику "в чем сложность поменять циферку, кому надо -- те обновятся" начинается бубнеж "я не буду поддерживать 2 версии". А его кто-то просит? Поддерживай последнюю, как сейчас и делаешь.

    PR уже давно даже не рассматриваются. Не удивлюсь, если автор вообще отписался от писем из репозитория и в черный список внес. Передать права на сопровождение, или хотя бы попросить помочь с сопровождением, если времени не хватает, что-то, видимо, мешает, на "Ч" начинается, на "В" заканчивается.

    Явные указания на баги закрываются с причиной "не баг, я так сказал". Плевать, что это приводит к проблемам. Признать свою ошибку совершенно невозможно.

    Более-менее сносно работает только serde_json. serde_yaml так вообще недавно объявлен неподдерживаемым (скорее всего как раз из-за первой проблемы). Причем автор даже не попросил сообщество взять на себя сопровождение. Молча заархивировал и все... Конечно можно форкнуть, но что это за поведение?

    Диалога с сообществом никакого. Автор сказал свое слово, автор закрыл возможность комментирования. Творит дичь (для проекта, на который завязано половина экосистемы раста). И то, похоже его кто-то в кулуарах образумил, а иначе бы была закрыта эта задача без возможности комментирования (впрочем, она и сейчас закрыта).

    Может Девид и семи пядей во лбу и что-то там двигает в компиляторе, но отношение к сообществу просто отвратительное.


    1. Ryav
      20.04.2024 16:41

      И какая альтернатива?


      1. Mingun
        20.04.2024 16:41
        +3

        К сожалению, реальной пока не видно. Если что-то очень мешает, то можно форкнуть, поправить баги и пользоваться секцией [patch] в своих проектах (чтобы в ваши зависимости исправления тоже втянулись). Если появится подобный форк и удастся его популяризовать, глядишь, может дело и сдвинется с мертвой точки. С node.js / io.js подобное прокатило