Привет, Хабр!
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)
Mingun
20.04.2024 16:41+9К сожалению, с развитием serde все очень плохо. Ошибки в дизайне не правятся и не собираются. Вместо следования semver (что вообще-то является обязательным для проектов cargo, т.к. именно так cargo обрабатывает поле
version
), автор наперекор всем делает ломающие изменения в патчах со словами "ну, замел под ковер, значит не ломающее". На справедливую критику "в чем сложность поменять циферку, кому надо -- те обновятся" начинается бубнеж "я не буду поддерживать 2 версии". А его кто-то просит? Поддерживай последнюю, как сейчас и делаешь.PR уже давно даже не рассматриваются. Не удивлюсь, если автор вообще отписался от писем из репозитория и в черный список внес. Передать права на сопровождение, или хотя бы попросить помочь с сопровождением, если времени не хватает, что-то, видимо, мешает, на "Ч" начинается, на "В" заканчивается.
Явные указания на баги закрываются с причиной "не баг, я так сказал". Плевать, что это приводит к проблемам. Признать свою ошибку совершенно невозможно.
Более-менее сносно работает только serde_json. serde_yaml так вообще недавно объявлен неподдерживаемым (скорее всего как раз из-за первой проблемы). Причем автор даже не попросил сообщество взять на себя сопровождение. Молча заархивировал и все... Конечно можно форкнуть, но что это за поведение?
Диалога с сообществом никакого. Автор сказал свое слово, автор закрыл возможность комментирования. Творит дичь (для проекта, на который завязано половина экосистемы раста). И то, похоже его кто-то в кулуарах образумил, а иначе бы была закрыта эта задача без возможности комментирования (впрочем, она и сейчас закрыта).
Может Девид и семи пядей во лбу и что-то там двигает в компиляторе, но отношение к сообществу просто отвратительное.
Ryav
20.04.2024 16:41И какая альтернатива?
Mingun
20.04.2024 16:41+3К сожалению, реальной пока не видно. Если что-то очень мешает, то можно форкнуть, поправить баги и пользоваться секцией
[patch]
в своих проектах (чтобы в ваши зависимости исправления тоже втянулись). Если появится подобный форк и удастся его популяризовать, глядишь, может дело и сдвинется с мертвой точки. С node.js / io.js подобное прокатило
sdramare
Это не статья, а выборочный пересказ документации. Дали бы тогда просто ссылку на https://serde.rs/ и все.