Bem-vindo à aula sobre serialização com serde! Nesta aula, vamos mergulhar em uma das bibliotecas mais importantes do ecossistema Rust: serde. A serialização é o processo de converter estruturas de dados em um formato que possa ser armazenado ou transmitido (como JSON, YAML, ou binário), e a desserialização é o processo inverso. Serde (abreviação de Serialization/Deserialization) é um framework que fornece uma maneira eficiente e genérica de fazer isso, com suporte a muitos formatos e personalizações.

Vamos aprender a usar os traits Serialize e Deserialize, trabalhar com o formato mais comum (JSON), explorar atributos para controlar a serialização e conhecer outros formatos suportados. Ao final, você terá uma base sólida para serializar dados em seus projetos Rust.

Serialize e Deserialize

Os pilares do serde são os traits Serialize e Deserialize. Quando você deriva esses traits em suas structs e enums, elas se tornam capazes de se converter em um formato serializado e de serem reconstruídas a partir dele. A derivação é feita com o atributo #[derive(Serialize, Deserialize)].

Esses traits são genéricos sobre o formato, o que significa que a mesma struct pode ser serializada em JSON, YAML, ou qualquer outro formato suportado sem alterar a definição. O serde cuida de mapear os campos da struct para os nomes e tipos correspondentes no formato alvo.

Vamos ver um exemplo básico:

use serde::{Serialize, Deserialize};

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

fn main() {
    let usuario = Usuario {
        nome: "Ana".to_string(),
        idade: 30,
        email: "ana@example.com".to_string(),
    };
    // Aqui podemos serializar para vários formatos
}

O trait Serialize é implementado automaticamente pela derivação, e o mesmo para Deserialize. Você não precisa escrever implementações manuais na maioria dos casos. Para tipos mais complexos, como enums com dados, a derivação também funciona bem.

serde_json

O crate serde_json é a implementação mais popular do formato JSON para serde. Ele fornece funções como to_string, to_vec, from_str e from_slice para serializar e desserializar dados. Para usar, adicione as dependências no Cargo.toml:

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

Exemplo prático de serialização para JSON:

use serde::{Serialize, Deserialize};
use serde_json;

#[derive(Serialize, Deserialize)]
struct Produto {
    id: u32,
    nome: String,
    preco: f64,
}

fn main() {
    let produto = Produto {
        id: 1,
        nome: "Teclado".to_string(),
        preco: 99.90,
    };

    // Serializa para uma string JSON
    let json = serde_json::to_string(&produto).unwrap();
    println!("{}", json); // {"id":1,"nome":"Teclado","preco":99.9}

    // Desserializa de volta para a struct
    let produto2: Produto = serde_json::from_str(&json).unwrap();
    println!("{:?}", produto2);
}

Além de structs, você pode serializar coleções, tuplas e até valores genéricos. O serde_json também oferece um tipo Value que representa qualquer JSON, útil para processamento dinâmico.

Atributos

O serde fornece uma série de atributos para personalizar a serialização e desserialização. Eles são colocados antes dos campos ou na própria struct/enum. Alguns dos mais úteis incluem:

  • #[serde(rename = "nome_alternativo")]: muda o nome do campo no formato serializado.
  • #[serde(rename_all = "camelCase")]: aplica uma regra de nomenclatura a todos os campos (ex.: snake_case para camelCase).
  • #[serde(default)]: usa o valor padrão do tipo se o campo estiver ausente na desserialização.
  • #[serde(skip_serializing_if = "Option::is_none")]: omite o campo se a condição for verdadeira.
  • #[serde(flatten)]: achata os campos de uma struct aninhada.

Exemplo com atributos:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Config {
    host: String,
    port: u16,
    #[serde(default = "default_timeout")]
    timeout: u64,
}

fn default_timeout() -> u64 { 30 }

fn main() {
    let json = r#"{"host":"localhost","port":8080}"#;
    let config: Config = serde_json::from_str(json).unwrap();
    println!("{}:{}", config.host, config.port); // timeout usa default 30
}

Esses atributos permitem adaptar suas estruturas de dados a diferentes formatos e requisitos sem alterar a lógica principal.

Formatos

Embora JSON seja o formato mais comum, o serde suporta muitos outros via crates adicionais. Alguns exemplos:

  • JSON (serde_json): formato textual leve.
  • YAML (serde_yaml): formato legível por humanos, com indentação.
  • TOML (toml): usado em configurações, como Cargo.toml.
  • MessagePack (rmp-serde): formato binário compacto.
  • BSON (bson): usado com MongoDB.
  • Postcard (postcard): binário muito compacto para sistemas embarcados.

Cada formato tem suas vantagens: JSON é amplamente usado em APIs web, YAML é mais legível para configurações, e formatos binários são mais rápidos e compactos. A beleza do serde é que você pode trocar de formato simplesmente mudando a função chamada, mantendo o mesmo código de dados.

Exemplo com YAML (adicione serde_yaml ao Cargo.toml):

use serde::{Serialize, Deserialize};

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

fn main() {
    let pessoa = Pessoa { nome: "Carlos".to_string(), idade: 25 };
    let yaml = serde_yaml::to_string(&pessoa).unwrap();
    println!("{}", yaml);
    let de: Pessoa = serde_yaml::from_str(&yaml).unwrap();
}

Boas Práticas e Observações

Ao trabalhar com serde, considere as seguintes boas práticas:

  • Use #[serde(default)] para campos que podem ser omitidos, evitando erros de desserialização.
  • Prefira tipos como Option para campos opcionais, permitindo que o serde trate a ausência corretamente.
  • Para enums, use #[serde(tag = "tipo")] para serialização com tag, facilitando a compatibilidade com formatos como JSON.
  • Teste a serialização e desserialização em casos de borda, como strings vazias ou números negativos.
  • Mantenha o formato de serialização estável para evitar quebras de compatibilidade em APIs.

Referências

Exercícios

  1. Exercício 1: Crie uma struct Ponto com campos x e y (f64) e serialize para JSON usando serde_json. Imprima o JSON resultante.
  2. ✓ Resposta:
    use serde::{Serialize, Deserialize};
    use serde_json;
    
    #[derive(Serialize, Deserialize)]
    struct Ponto {
        x: f64,
        y: f64,
    }
    
    fn main() {
        let p = Ponto { x: 1.0, y: 2.0 };
        let json = serde_json::to_string(&p).unwrap();
        println!("{}", json);
    }
  3. Exercício 2: Usando a struct do exercício 1, desserialize a string JSON {"x":3.5,"y":4.5} em um valor Ponto e imprima os campos.
  4. ✓ Resposta:
    use serde::{Deserialize};
    use serde_json;
    
    #[derive(Deserialize)]
    struct Ponto {
        x: f64,
        y: f64,
    }
    
    fn main() {
        let json = r#"{"x":3.5,"y":4.5}"#;
        let p: Ponto = serde_json::from_str(json).unwrap();
        println!("x: {}, y: {}", p.x, p.y);
    }
  5. Exercício 3: Adicione um atributo #[serde(rename_all = "camelCase")] a uma struct com campos nome_completo e idade_usuario, serialize e observe a saída JSON.
  6. ✓ Resposta:
    use serde::{Serialize, Deserialize};
    use serde_json;
    
    #[derive(Serialize, Deserialize)]
    #[serde(rename_all = "camelCase")]
    struct Usuario {
        nome_completo: String,
        idade_usuario: u8,
    }
    
    fn main() {
        let u = Usuario {
            nome_completo: "Maria".to_string(),
            idade_usuario: 28,
        };
        let json = serde_json::to_string(&u).unwrap();
        println!("{}", json); // {"nomeCompleto":"Maria","idadeUsuario":28}
    }
  7. Exercício 4: Crie uma enum Status com variantes Ativo, Inativo e Pendente, e serialize para JSON. Use #[serde(rename_all = "lowercase")] para que os valores sejam minúsculos.
  8. ✓ Resposta:
    use serde::{Serialize, Deserialize};
    use serde_json;
    
    #[derive(Serialize, Deserialize)]
    #[serde(rename_all = "lowercase")]
    enum Status {
        Ativo,
        Inativo,
        Pendente,
    }
    
    fn main() {
        let status = Status::Ativo;
        let json = serde_json::to_string(&status).unwrap();
        println!("{}", json); // "ativo"
    }
    
  9. Exercício 5: Use o atributo #[serde(default)] em um campo do tipo u32 e desserialize um JSON sem esse campo, verificando que o valor padrão (0) é usado.
  10. ✓ Resposta:
    use serde::{Deserialize};
    use serde_json;
    
    #[derive(Deserialize)]
    struct Config {
        #[serde(default)]
        port: u32,
    }
    
    fn main() {
        let json = r#"{}"#;
        let config: Config = serde_json::from_str(json).unwrap();
        println!("port: {}", config.port); // 0
    }