Rust serde: сериализация и десериализация данных — tips и подводные камни
Serde — самая используемая библиотека в экосистеме Rust. Она обеспечивает сериализацию и десериализацию в любой формат: JSON, TOML, YAML, MessagePack, bincode. Derive-макросы делают базовое использование тривиальным, но за простотой скрываются десятки атрибутов и неочевидных поведений.
Основы: derive макросы
Минимальный пример — структура с автоматической сериализацией:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: u64,
name: String,
email: String,
#[serde(default)]
is_active: bool,
}
let json = r#"{"id": 1, "name": "Alice", "email": "alice@example.com"}"#;
let user: User = serde_json::from_str(json).unwrap();
// is_active = false (default)
Derive генерирует реализации трейтов Serialize и Deserialize на этапе компиляции — zero runtime cost для простых случаев.
Атрибуты: тонкая настройка
Самые полезные атрибуты на уровне контейнера и полей:
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // snake_case -> camelCase в JSON
struct ApiResponse {
user_name: String, // -> "userName"
created_at: String, // -> "createdAt"
#[serde(skip)] // не сериализуется/десериализуется
internal_cache: Vec<u8>,
#[serde(default = "default_page")]
page_size: u32, // если отсутствует — вызовет default_page()
#[serde(alias = "user_id")] // принимает и "userId" и "user_id"
#[serde(alias = "uid")]
user_id: u64,
#[serde(flatten)] // «разворачивает» вложенную структуру
metadata: Metadata,
}
fn default_page() -> u32 { 20 }
flatten особенно полезен для расширяемых API — дополнительные поля из Metadata появляются на верхнем уровне JSON, а не как вложенный объект.
Кастомная сериализация
Когда стандартного поведения недостаточно — serialize_with и deserialize_with:
use chrono::{DateTime, Utc};
#[derive(Serialize, Deserialize)]
struct Event {
name: String,
#[serde(serialize_with = "serialize_dt", deserialize_with = "deserialize_dt")]
timestamp: DateTime<Utc>,
}
fn serialize_dt<S>(dt: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
s.serialize_i64(dt.timestamp_millis())
}
fn deserialize_dt<'de, D>(d: D) -> Result<DateTime<Utc>, D::Error>
where D: serde::Deserializer<'de> {
let millis = i64::deserialize(d)?;
DateTime::from_timestamp_millis(millis)
.ok_or_else(|| serde::de::Error::custom("invalid timestamp"))
}
Типичные случаи: даты как unix timestamp, числа как строки, base64-кодирование байтов.
Enum representations
Serde поддерживает четыре способа представления enum в JSON:
// Externally tagged (по умолчанию): {"Text": {"body": "hello"}}
#[derive(Serialize, Deserialize)]
enum Message {
Text { body: String },
Image { url: String },
}
// Internally tagged: {"type": "text", "body": "hello"}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Event {
UserCreated { user_id: u64 },
OrderPlaced { order_id: u64, amount: f64 },
}
// Adjacently tagged: {"t": "text", "c": {"body": "hello"}}
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum Payload {
Text { body: String },
Binary(Vec<u8>),
}
// Untagged: {"body": "hello"} — serde пробует варианты по порядку
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
Int(i64),
Float(f64),
Str(String),
}
Internally tagged — самый популярный для API. Untagged удобен для парсинга «полиморфных» JSON, но ошибки десериализации будут неинформативными.
Производительность
Советы для горячих путей:
- simd-json — drop-in замена serde_json с SIMD-ускорением, на 2-3x быстрее на парсинге
- Zero-copy с Cow — избегайте аллокаций при десериализации:
use std::borrow::Cow;
#[derive(Deserialize)]
struct Record<'a> {
#[serde(borrow)]
name: Cow<'a, str>, // заимствует из входного буфера, не копирует
#[serde(borrow)]
tags: Vec<Cow<'a, str>>,
}
- Bytes — для бинарных данных используйте
serde_bytesвместоVec<u8> - Буферизация —
from_readerмедленнее чемfrom_slice. Читайте файл в память, потом парсите
Подводные камни
- Option<Option<T>> —
None= поле отсутствует,Some(None)= поле есть со значением null. Полезно для PATCH-запросов, но неочевидно - deny_unknown_fields — строгая десериализация, отклоняет лишние поля. Ломает обратную совместимость при добавлении полей в API
- Большие структуры — derive для структур с 50+ полями замедляет компиляцию. Рассмотрите ручную реализацию или разбиение на вложенные структуры
- flatten + deny_unknown_fields — не работают вместе корректно (известный баг serde)
- Default для enum —
#[serde(default)]на enum-поле требуетimpl Defaultдля enum, что не всегда логично
// PATCH-паттерн с Option<Option<T>>
#[derive(Deserialize)]
struct UpdateUser {
#[serde(default, deserialize_with = "deserialize_optional_field")]
name: Option<Option<String>>,
// None = не менять, Some(None) = обнулить, Some(Some(v)) = установить v
}
Итог
Serde покрывает 95% задач сериализации через derive и атрибуты. Для остального — serialize_with и ручные реализации. Выбирайте правильное представление enum и не забывайте про #[serde(default)] для обратной совместимости.