Serde Rust: Guia Completo de Serialização JSON | Rust Brasil

Guia completo do Serde em Rust: JSON, TOML, YAML e MessagePack. Custom serializers, derive e dicas de performance.

Introdução

Serde (abreviação de Serialize + Deserialize) é a biblioteca de serialização mais importante do ecossistema Rust. Ela fornece um framework genérico e de alta performance para converter estruturas de dados Rust de e para diversos formatos como JSON, TOML, YAML, MessagePack, Bincode e muitos outros.

A magia do Serde está na sua arquitetura: o framework é separado dos formatos. Você define como seus tipos se serializam uma vez (geralmente via #[derive]), e isso funciona automaticamente com qualquer formato suportado. Essa separação permite que novos formatos sejam adicionados sem alterar o código das suas structs.

O Serde é usado por praticamente toda crate relevante do ecossistema Rust — de frameworks web como Axum e Actix até ferramentas de configuração, bancos de dados e ferramentas CLI.

Dependências no Cargo.toml

[dependencies]
serde = { version = "1", features = ["derive"] }

# Formatos (adicione conforme a necessidade)
serde_json = "1"      # JSON
toml = "0.8"           # TOML
serde_yaml = "0.9"     # YAML
rmp-serde = "1"        # MessagePack
bincode = "1"          # Bincode (binário compacto)
csv = "1"              # CSV

Derive Básico: Serialize e Deserialize

A forma mais comum de usar o Serde é com derives:

use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Usuario {
    nome: String,
    email: String,
    idade: u32,
    ativo: bool,
}

fn main() {
    let usuario = Usuario {
        nome: "Maria Silva".to_string(),
        email: "maria@exemplo.com".to_string(),
        idade: 28,
        ativo: true,
    };

    // Serializar para JSON
    let json = serde_json::to_string_pretty(&usuario).unwrap();
    println!("{json}");

    // Deserializar de JSON
    let de_volta: Usuario = serde_json::from_str(&json).unwrap();
    println!("{:?}", de_volta);
}

Saída:

{
  "nome": "Maria Silva",
  "email": "maria@exemplo.com",
  "idade": 28,
  "ativo": true
}

Atributos do Serde

O Serde oferece dezenas de atributos para personalizar a serialização. Aqui estão os mais importantes:

Renomear Campos

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ConfigServidor {
    porta_http: u16,          // -> "portaHttp"
    max_conexoes: u32,        // -> "maxConexoes"
    tempo_timeout: u64,       // -> "tempoTimeout"
}

// Outras opções: "snake_case", "SCREAMING_SNAKE_CASE",
//                "kebab-case", "PascalCase"

Campos Opcionais e Valores Padrão

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Config {
    host: String,

    #[serde(default = "porta_padrao")]
    porta: u16,

    #[serde(skip_serializing_if = "Option::is_none")]
    descricao: Option<String>,

    #[serde(default)]
    debug: bool,
}

fn porta_padrao() -> u16 {
    8080
}

Flatten e Rename

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Endereco {
    rua: String,
    cidade: String,
    estado: String,
}

#[derive(Serialize, Deserialize)]
struct Pessoa {
    nome: String,

    #[serde(rename = "email_principal")]
    email: String,

    #[serde(flatten)]
    endereco: Endereco,
}

O flatten “achata” os campos de Endereco no nível superior:

{
  "nome": "João",
  "email_principal": "joao@ex.com",
  "rua": "Rua A, 123",
  "cidade": "São Paulo",
  "estado": "SP"
}

Enums com Tags

use serde::{Serialize, Deserialize};

// Tag externa (padrão)
#[derive(Serialize, Deserialize)]
enum Evento {
    UsuarioCriado { id: u64, nome: String },
    PedidoRealizado { id: u64, valor: f64 },
    PagamentoRecebido { pedido_id: u64 },
}
// {"UsuarioCriado": {"id": 1, "nome": "Ana"}}

// Tag interna
#[derive(Serialize, Deserialize)]
#[serde(tag = "tipo")]
enum EventoInterno {
    UsuarioCriado { id: u64, nome: String },
    PedidoRealizado { id: u64, valor: f64 },
}
// {"tipo": "UsuarioCriado", "id": 1, "nome": "Ana"}

// Tag adjacente
#[derive(Serialize, Deserialize)]
#[serde(tag = "tipo", content = "dados")]
enum EventoAdjacente {
    UsuarioCriado { id: u64, nome: String },
    PedidoRealizado { id: u64, valor: f64 },
}
// {"tipo": "UsuarioCriado", "dados": {"id": 1, "nome": "Ana"}}

// Sem tag (untagged)
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Valor {
    Inteiro(i64),
    Texto(String),
    Lista(Vec<String>),
}
// Tenta deserializar cada variante em ordem

Multi-formato: O Mesmo Struct em Vários Formatos

Uma das maiores forças do Serde é que o mesmo derive funciona com qualquer formato:

use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Configuracao {
    nome: String,
    versao: String,
    porta: u16,
    recursos: Vec<String>,
}

fn main() {
    let config = Configuracao {
        nome: "meu-app".to_string(),
        versao: "1.0.0".to_string(),
        porta: 8080,
        recursos: vec!["auth".to_string(), "cache".to_string()],
    };

    // JSON
    let json = serde_json::to_string_pretty(&config).unwrap();
    println!("=== JSON ===\n{json}\n");

    // TOML
    let toml_str = toml::to_string_pretty(&config).unwrap();
    println!("=== TOML ===\n{toml_str}\n");

    // YAML
    let yaml = serde_yaml::to_string(&config).unwrap();
    println!("=== YAML ===\n{yaml}\n");

    // MessagePack (binário)
    let msgpack = rmp_serde::to_vec(&config).unwrap();
    println!("=== MessagePack ===\n{} bytes\n", msgpack.len());

    // Deserializar de volta (qualquer formato)
    let de_json: Configuracao = serde_json::from_str(&json).unwrap();
    let de_toml: Configuracao = toml::from_str(&toml_str).unwrap();
    let de_yaml: Configuracao = serde_yaml::from_str(&yaml).unwrap();
    let de_msgpack: Configuracao = rmp_serde::from_slice(&msgpack).unwrap();

    println!("{:?}", de_json);
    println!("{:?}", de_toml);
    println!("{:?}", de_yaml);
    println!("{:?}", de_msgpack);
}

Serialização Customizada

Para casos onde o derive não é suficiente, você pode implementar serialização personalizada:

Custom Serialize para um Campo

use serde::{Serialize, Deserialize, Serializer, Deserializer};
use chrono::NaiveDateTime;

#[derive(Debug, Serialize, Deserialize)]
struct LogEntry {
    mensagem: String,

    #[serde(serialize_with = "serializar_data", deserialize_with = "deserializar_data")]
    timestamp: NaiveDateTime,
}

fn serializar_data<S>(data: &NaiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let formatado = data.format("%d/%m/%Y %H:%M:%S").to_string();
    serializer.serialize_str(&formatado)
}

fn deserializar_data<'de, D>(deserializer: D) -> Result<NaiveDateTime, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    NaiveDateTime::parse_from_str(&s, "%d/%m/%Y %H:%M:%S")
        .map_err(serde::de::Error::custom)
}

Implementação Manual Completa

use serde::{Serialize, Serializer};
use serde::ser::SerializeStruct;

struct Cor {
    r: u8,
    g: u8,
    b: u8,
}

impl Serialize for Cor {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serializar como string hexadecimal
        let hex = format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b);
        serializer.serialize_str(&hex)
    }
}

fn main() {
    let cor = Cor { r: 255, g: 128, b: 0 };
    let json = serde_json::to_string(&cor).unwrap();
    println!("{json}"); // "#ff8000"
}

Deserializar de Múltiplos Formatos

use serde::{Deserialize, Deserializer};

#[derive(Debug, Deserialize)]
struct Config {
    #[serde(deserialize_with = "string_ou_numero")]
    porta: u16,
}

fn string_ou_numero<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum StringOuNumero {
        Str(String),
        Num(u16),
    }

    match StringOuNumero::deserialize(deserializer)? {
        StringOuNumero::Str(s) => s.parse().map_err(serde::de::Error::custom),
        StringOuNumero::Num(n) => Ok(n),
    }
}

fn main() {
    // Ambos funcionam:
    let c1: Config = serde_json::from_str(r#"{"porta": 8080}"#).unwrap();
    let c2: Config = serde_json::from_str(r#"{"porta": "8080"}"#).unwrap();
    println!("{:?} {:?}", c1, c2);
}

Trabalhando com JSON Dinâmico

Quando você não conhece a estrutura do JSON antecipadamente:

use serde_json::Value;

fn main() {
    let json_str = r#"{
        "nome": "Ana",
        "idade": 30,
        "hobbies": ["leitura", "programação"],
        "endereco": {
            "cidade": "São Paulo"
        }
    }"#;

    // Parse para Value genérico
    let valor: Value = serde_json::from_str(json_str).unwrap();

    // Acessar campos
    println!("Nome: {}", valor["nome"]);
    println!("Cidade: {}", valor["endereco"]["cidade"]);

    // Verificar tipo
    if let Some(hobbies) = valor["hobbies"].as_array() {
        for hobby in hobbies {
            println!("Hobby: {}", hobby.as_str().unwrap_or_default());
        }
    }

    // Construir JSON com macro
    let novo = serde_json::json!({
        "status": "ok",
        "dados": {
            "total": 42,
            "itens": ["a", "b", "c"]
        }
    });
    println!("{}", serde_json::to_string_pretty(&novo).unwrap());
}

Performance

O Serde é extremamente performático. Benchmarks comparativos:

Operação (JSON)serde_json (Rust)Jackson (Java)json (Python)
Serialização~500 MB/s~300 MB/s~50 MB/s
Deserialização~400 MB/s~250 MB/s~40 MB/s

Dicas de Performance

  1. Use &str ao invés de String quando possível na deserialização:
use serde::Deserialize;

// Rápido: zero-copy para strings simples
#[derive(Deserialize)]
struct Rapido<'a> {
    #[serde(borrow)]
    nome: &'a str,
}

// Mais lento: aloca String
#[derive(Deserialize)]
struct Normal {
    nome: String,
}
  1. Use serde_json::from_reader para streams ao invés de carregar tudo na memória:
use std::fs::File;
use std::io::BufReader;

fn ler_config() -> Result<Config, Box<dyn std::error::Error>> {
    let file = File::open("config.json")?;
    let reader = BufReader::new(file);
    let config = serde_json::from_reader(reader)?;
    Ok(config)
}
  1. Use Bincode ou MessagePack para comunicação entre serviços (muito mais rápido que JSON):
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Mensagem {
    id: u64,
    conteudo: String,
}

fn main() {
    let msg = Mensagem { id: 1, conteudo: "teste".to_string() };

    // Bincode: ~10x mais rápido que JSON
    let bytes = bincode::serialize(&msg).unwrap();
    println!("Bincode: {} bytes", bytes.len());

    // MessagePack: compacto e cross-language
    let mp = rmp_serde::to_vec(&msg).unwrap();
    println!("MsgPack: {} bytes", mp.len());

    // JSON: legível, mas maior
    let json = serde_json::to_vec(&msg).unwrap();
    println!("JSON: {} bytes", json.len());
}

Integração com Frameworks Web

O Serde é a base de serialização para todos os frameworks web em Rust:

// Axum: extractors e respostas JSON automáticas
use axum::{extract::Json, routing::post, Router};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct Input {
    texto: String,
}

#[derive(Serialize)]
struct Output {
    resultado: String,
    tamanho: usize,
}

async fn processar(Json(input): Json<Input>) -> Json<Output> {
    Json(Output {
        tamanho: input.texto.len(),
        resultado: input.texto.to_uppercase(),
    })
}

Para mais sobre Axum, veja Axum vs Actix Web.

Padrões e Boas Práticas

  1. Sempre use #[serde(rename_all = "camelCase")] ao expor APIs JSON para frontends JavaScript
  2. Use #[serde(skip_serializing_if = "Option::is_none")] para campos opcionais
  3. Use #[serde(default)] para backward compatibility ao adicionar novos campos
  4. Separe structs de entrada e saída — não use o mesmo struct para request e response
  5. Use #[serde(deny_unknown_fields)] quando quiser ser estrito na validação
use serde::Deserialize;

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RequestEstrito {
    nome: String,
    email: String,
    // Qualquer campo não listado aqui causará erro na deserialização
}

Conclusão

O Serde é uma peça fundamental do ecossistema Rust. Sua arquitetura de zero-cost abstractions permite serialização e deserialização de alta performance em qualquer formato, com uma API ergonômica baseada em derives. Dominar o Serde e seus atributos é essencial para qualquer desenvolvedor Rust.

Veja Também