lenec ru

← все посты

Rust serde: сериализация и десериализация данных — tips и подводные камни

12K

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)] для обратной совместимости.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.