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
- Use
&strao invés deStringquando 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,
}
- Use
serde_json::from_readerpara 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)
}
- 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
- Sempre use
#[serde(rename_all = "camelCase")]ao expor APIs JSON para frontends JavaScript - Use
#[serde(skip_serializing_if = "Option::is_none")]para campos opcionais - Use
#[serde(default)]para backward compatibility ao adicionar novos campos - Separe structs de entrada e saída — não use o mesmo struct para request e response
- 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.