Serde: serialização e desserialização em Rust
1. Introdução ao Serde
Serde é um framework de serialização e desserialização para Rust, considerado essencial no ecossistema da linguagem por sua eficiência, segurança de tipos e flexibilidade. Ele permite converter estruturas de dados Rust em formatos intercambiáveis (como JSON, YAML, TOML) e vice-versa, sem sacrificar a segurança de tipos que Rust oferece.
O ecossistema Serde é composto por três componentes principais:
- serde: o núcleo do framework, com os traits Serialize e Deserialize
- serde_derive: um crate de macros que gera implementações automaticamente via #[derive]
- Crates de formato: como serde_json, serde_yaml e toml, que implementam os formatos de dados
Para começar, adicione as dependências no Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
toml = "0.8"
2. Serialização e Desserialização com Derive
A forma mais comum de usar Serde é através do #[derive(Serialize, Deserialize)]. Vamos ver um exemplo prático:
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Usuario {
nome: String,
idade: u8,
email: String,
#[serde(skip_serializing_if = "Option::is_none")]
telefone: Option<String>,
#[serde(default = "default_pais")]
pais: String,
}
fn default_pais() -> String {
"Brasil".to_string()
}
fn main() {
let usuario = Usuario {
nome: "Ana Silva".into(),
idade: 28,
email: "ana@exemplo.com".into(),
telefone: None,
pais: "Brasil".into(),
};
let json = serde_json::to_string_pretty(&usuario).unwrap();
println!("JSON:\n{}", json);
}
Atributos importantes para personalização:
#[derive(Serialize, Deserialize)]
struct Config {
#[serde(rename = "db_host")]
database_host: String,
#[serde(rename = "db_port")]
database_port: u16,
#[serde(default = "default_timeout")]
timeout_seconds: u64,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "tipo", content = "dados")]
enum Mensagem {
Texto { conteudo: String },
Imagem { url: String, tamanho: u32 },
Comando(String),
}
fn main() {
let msg = Mensagem::Texto { conteudo: "Olá".into() };
let json = serde_json::to_string(&msg).unwrap();
println!("{}", json); // {"tipo":"Texto","dados":{"conteudo":"Olá"}}
}
3. Trabalhando com Tipos Complexos
Serde lida naturalmente com coleções e tipos aninhados:
use std::collections::HashMap;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Turma {
nome: String,
alunos: Vec<Aluno>,
notas: HashMap<String, Vec<f64>>,
metadata: Option<TurmaMetadata>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Aluno {
nome: String,
matricula: u32,
ativo: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct TurmaMetadata {
ano: u16,
semestre: u8,
}
// Conversão automática com from/into
#[derive(Debug, Serialize, Deserialize)]
#[serde(into = "String", from = "String")]
struct Cpf(String);
impl From<Cpf> for String {
fn from(cpf: Cpf) -> String {
cpf.0
}
}
impl From<String> for Cpf {
fn from(s: String) -> Cpf {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
Cpf(digits)
}
}
4. Formatos de Dados com Serde
JSON com serde_json
use serde_json::{Value, json};
fn main() {
// Serialização
let dados = json!({
"nome": "Maria",
"idade": 32,
"habilidades": ["Rust", "Python", "SQL"]
});
println!("{}", serde_json::to_string_pretty(&dados).unwrap());
// Manipulação de Value
if let Some(nome) = dados.get("nome").and_then(|v| v.as_str()) {
println!("Nome: {}", nome);
}
// Desserialização para struct
#[derive(Deserialize)]
struct Pessoa {
nome: String,
idade: u8,
}
let json_str = r#"{"nome":"João","idade":25}"#;
let pessoa: Pessoa = serde_json::from_str(json_str).unwrap();
}
YAML com serde_yaml
use serde_yaml;
#[derive(Debug, Serialize, Deserialize)]
struct ConfigApp {
servidor: ServidorConfig,
banco: BancoConfig,
}
#[derive(Debug, Serialize, Deserialize)]
struct ServidorConfig {
host: String,
porta: u16,
}
#[derive(Debug, Serialize, Deserialize)]
struct BancoConfig {
url: String,
pool_size: u32,
}
fn main() {
let yaml_str = r#"
servidor:
host: "127.0.0.1"
porta: 8080
banco:
url: "postgres://localhost/mydb"
pool_size: 10
"#;
let config: ConfigApp = serde_yaml::from_str(yaml_str).unwrap();
println!("{:?}", config);
}
TOML com toml
use toml;
#[derive(Debug, Serialize, Deserialize)]
struct Configuracao {
titulo: String,
autor: String,
versao: String,
[serde(default)]
dependencias: Vec<String>,
}
fn main() {
let toml_str = r#"
titulo = "Meu Projeto"
autor = "João"
versao = "1.0.0"
dependencias = ["serde", "tokio", "reqwest"]
"#;
let config: Configuracao = toml::from_str(toml_str).unwrap();
println!("Título: {}", config.titulo);
// Serializar de volta para TOML
let toml_output = toml::to_string(&config).unwrap();
println!("{}", toml_output);
}
5. Customização Avançada com Traits
Para controle total, implemente manualmente os traits:
use serde::{Serialize, Serializer, Deserialize, Deserializer, de};
struct DataHoraCustomizada {
timestamp: i64,
timezone: String,
}
impl Serialize for DataHoraCustomizada {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let formatted = format!("{} [{}]", self.timestamp, self.timezone);
serializer.serialize_str(&formatted)
}
}
impl<'de> Deserialize<'de> for DataHoraCustomizada {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let parts: Vec<&str> = s.split(" [").collect();
if parts.len() != 2 {
return Err(de::Error::custom("formato inválido: esperado 'timestamp [timezone]'"));
}
let timestamp = parts[0].parse::<i64>()
.map_err(|_| de::Error::custom("timestamp inválido"))?;
let timezone = parts[1].trim_end_matches(']').to_string();
Ok(DataHoraCustomizada { timestamp, timezone })
}
}
6. Estratégias de Performance e Segurança
Uso de flatten para estruturas aninhadas
#[derive(Debug, Serialize, Deserialize)]
struct Endereco {
rua: String,
cidade: String,
cep: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct PessoaCompleta {
nome: String,
idade: u8,
#[serde(flatten)]
endereco: Endereco,
}
// Resulta em JSON plano: {"nome":"...","idade":...,"rua":"...","cidade":"...","cep":"..."}
Validação durante desserialização
use serde::Deserialize;
fn validar_email<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if !s.contains('@') {
return Err(serde::de::Error::custom("email inválido: deve conter @"));
}
Ok(s)
}
#[derive(Debug, Deserialize)]
struct UsuarioValido {
nome: String,
#[serde(deserialize_with = "validar_email")]
email: String,
}
7. Casos de Uso Práticos
Configuração de aplicações com fallback
use std::fs;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct ConfigApp {
#[serde(default = "default_host")]
host: String,
#[serde(default = "default_porta")]
porta: u16,
#[serde(default)]
debug: bool,
}
fn default_host() -> String { "localhost".into() }
fn default_porta() -> u16 { 8080 }
fn carregar_config() -> ConfigApp {
// Tenta carregar de arquivo, usa defaults se falhar
let conteudo = fs::read_to_string("config.toml")
.unwrap_or_else(|_| String::new());
toml::from_str(&conteudo).unwrap_or_else(|_| ConfigApp {
host: default_host(),
porta: default_porta(),
debug: false,
})
}
Comunicação com APIs REST
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize)]
struct LoginRequest {
username: String,
password: String,
}
#[derive(Debug, Deserialize)]
struct LoginResponse {
token: String,
expires_in: u64,
#[serde(default)]
refresh_token: Option<String>,
}
async fn fazer_login(username: &str, password: &str) -> Result<LoginResponse, reqwest::Error> {
let client = reqwest::Client::new();
let request = LoginRequest {
username: username.into(),
password: password.into(),
};
let response = client
.post("https://api.exemplo.com/login")
.json(&request)
.send()
.await?
.json::<LoginResponse>()
.await?;
Ok(response)
}
8. Boas Práticas e Troubleshooting
Erros comuns e soluções
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Produto {
nome: String,
preco: f64,
#[serde(default)]
categoria: String,
#[serde(deny_unknown_fields)] // Rejeita campos extras
estoque: u32,
}
// Erro: missing field `estoque`
// Solução: adicionar #[serde(default)] ou garantir presença
// Erro: invalid type: string "abc", expected u32 for field `estoque`
// Solução: usar deserialize_with para conversão personalizada
// Versionamento de esquemas com untagged
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum VersaoConfig {
V1(ConfigV1),
V2(ConfigV2),
}
#[derive(Debug, Deserialize)]
struct ConfigV1 {
nome: String,
versao: u8,
}
#[derive(Debug, Deserialize)]
struct ConfigV2 {
nome: String,
versao: u8,
descricao: String,
}
Dicas de otimização
use std::borrow::Cow;
#[derive(Debug, Serialize, Deserialize)]
struct MensagemOtimizada<'a> {
#[serde(borrow)]
texto: Cow<'a, str>,
prioridade: u8,
}
// Evita alocações desnecessárias ao desserializar strings que podem ser emprestadas
Referências
- Documentação oficial do Serde — Guia completo com exemplos, atributos e referência da API
- serde_json crate documentation — Documentação detalhada do crate serde_json com todos os tipos e funções
- The Serde Book - Rust Serialization Framework — Tutorial oficial sobre o uso de derive para serialização e desserialização
- Rust by Example: Serde — Exemplos práticos de uso do Serde no contexto do aprendizado de Rust
- serde_yaml crate documentation — Documentação do crate serde_yaml para serialização YAML
- toml crate documentation — Documentação oficial do crate toml para trabalhar com arquivos de configuração TOML
- Rust Design Patterns: Serialization — Padrões de design relacionados à serialização em Rust, incluindo boas práticas com Serde