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