JSON com Serde em Rust
JSON está em todo lugar: arquivos de configuração, APIs web, jogos salvos. Em Rust, praticamente todo mundo usa dois crates para trabalhar com ele. O serde (abreviação de serialize / deserialize, ou seja, serializar e desserializar) sabe transformar valores do Rust em dados e vice-versa. O serde_json ensina a ele o formato JSON.
Pense no serde como um tradutor. Você descreve seus dados uma única vez, como uma struct normal do Rust, e o serde traduz para JSON e de volta para você. Você nunca escreve o texto JSON na mão, e nunca precisa desmontar o JSON caractere por caractere.
Preparando o projeto
Seção intitulada “Preparando o projeto”Dentro do seu projeto, adicione os dois crates. A feature derive faz o serde escrever o código de tradução para você:
cargo add serde --features derivecargo add serde_jsonAgora o seu Cargo.toml tem:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Se Cargo.toml e cargo add são novidade para você, leia antes a página Cargo e crates. O Rust Playground já vem com os dois crates, então todos os botões “Experimente você mesmo” desta página funcionam sem preparar nada.
De struct para JSON
Seção intitulada “De struct para JSON”Coloque #[derive(Serialize)] em uma struct, e o serde_json::to_string consegue transformá-la em texto JSON:
use serde::Serialize;
#[derive(Serialize)]struct User { name: String, age: u32, admin: bool,}
fn main() { let user = User { name: String::from("Ana"), age: 28, admin: false };
let compact = serde_json::to_string(&user).unwrap(); println!("{}", compact);
let pretty = serde_json::to_string_pretty(&user).unwrap(); println!("{}", pretty);}Saída:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}O to_string gera um JSON compacto, bom para enviar pela rede. O to_string_pretty acrescenta quebras de linha e espaços, o que fica melhor em arquivos que pessoas vão ler. As chaves seguem a mesma ordem dos campos da sua struct.
De JSON para struct
Seção intitulada “De JSON para struct”O caminho inverso se chama desserializar. Acrescente Deserialize à lista do derive e chame serde_json::from_str. Você diz qual tipo quer com uma anotação de tipo:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]struct User { name: String, age: u32, admin: bool,}
fn main() -> Result<(), serde_json::Error> { let text = r#"{"name": "Ana", "age": 28, "admin": false}"#;
let user: User = serde_json::from_str(text)?; println!("{:?}", user); println!("{} tem {} anos", user.name, user.age); Ok(())}Saída:
User { name: "Ana", age: 28, admin: false }Ana tem 28 anosRepare em duas coisas:
r#"..."#é uma raw string (string bruta). Dentro dela, você pode escrever"à vontade, o que ajuda muito com JSON.from_strdevolve umResult, porque o texto pode não ser um JSON válido. Aqui omaindevolveResult, então podemos usar?para repassar qualquer erro adiante.
Quando o JSON está errado
Seção intitulada “Quando o JSON está errado”JSON de verdade muitas vezes não vem do jeito que você espera. Falta um campo, ou um número chega como texto. O serde confere tudo e devolve um erro claro em vez de travar. Use match para tratar:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct User { name: String, age: u32,}
fn main() { let inputs = [ r#"{"name": "Ana", "age": 28}"#, r#"{"name": "Ben"}"#, r#"{"name": "Cy", "age": "old"}"#, ];
for text in inputs { match serde_json::from_str::<User>(text) { Ok(user) => println!("OK: {:?}", user), Err(e) => println!("Erro: {}", e), } }}Saída:
OK: User { name: "Ana", age: 28 }Erro: missing field `age` at line 1 column 15Erro: invalid type: string "old", expected u32 at line 1 column 27As mensagens de erro vêm do próprio serde_json, em inglês. A primeira diz que falta o campo age (linha 1, coluna 15). A segunda diz que o tipo é inválido: chegou a string "old" (“velho”), mas era esperado um u32 (linha 1, coluna 27).
A parte ::<User> se chama “turbofish”. É outra forma de dizer qual tipo você quer, útil quando não existe um let user: User onde colocar a anotação.
Chaves ausentes: Option e #[serde(default)]
Seção intitulada “Chaves ausentes: Option e #[serde(default)]”E se um campo for mesmo opcional? Você tem duas boas opções:
- Fazer do campo um
Option. Uma chave ausente (ounull) viraNone. - Acrescentar
#[serde(default)]. Uma chave ausente recebe o valor padrão do tipo:0,false, umaStringvazia ou umVecvazio.
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Profile { name: String, bio: Option<String>, #[serde(default)] followers: u32, #[serde(default)] tags: Vec<String>,}
fn main() { let full = r#"{"name": "Ana", "bio": "Adora Rust", "followers": 120, "tags": ["rust"]}"#; let short = r#"{"name": "Ben"}"#;
let a: Profile = serde_json::from_str(full).unwrap(); let b: Profile = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Saída:
Profile { name: "Ana", bio: Some("Adora Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }Use Option quando “sem valor” significa algo diferente de “zero” ou “vazio”. Use default quando o valor padrão já serve.
Renomeando campos
Seção intitulada “Renomeando campos”O Rust usa snake_case nos nomes de campos, mas muitas APIs web usam camelCase, e algumas usam nomes que nem são válidos em Rust (como type). Você não precisa abrir mão de nomes bonitos no Rust. Use rename_all para a struct inteira e rename para um campo só:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct UserProfile { user_id: u64, display_name: String, is_verified: bool, #[serde(rename = "type")] account_type: String,}
fn main() { // o tipo de resposta que uma API web poderia devolver let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{} (id {}) tem uma conta {}", profile.display_name, profile.user_id, profile.account_type);
// a serialização usa os mesmos nomes, então o JSON vai e volta igual println!("{}", serde_json::to_string(&profile).unwrap());}Saída:
Ana Silva (id 42) tem uma conta premium{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}Por padrão, o serde simplesmente ignora as chaves do JSON que a sua struct não tem. Então você só precisa descrever os campos que interessam, mesmo que a API mande cinquenta.
Listas: Vec de structs
Seção intitulada “Listas: Vec de structs”Um array JSON vira um Vec. Structs também podem conter outras structs e vetores, então você descreve um JSON cheio de níveis só aninhando tipos:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "Aprender serde", "done": true}, {"title": "Escrever um arquivo de configuração", "done": false}, {"title": "Chamar uma API", "done": false} ]"#;
let tasks: Vec<Task> = serde_json::from_str(text).unwrap();
for task in &tasks { let mark = if task.done { "x" } else { " " }; println!("[{}] {}", mark, task.title); } let left = tasks.iter().filter(|t| !t.done).count(); println!("{} tarefas restantes", left);}Saída:
[x] Aprender serde[ ] Escrever um arquivo de configuração[ ] Chamar uma API2 tarefas restantesJSON sem tipo: Value e json!
Seção intitulada “JSON sem tipo: Value e json!”Às vezes você não sabe de antemão o formato do JSON, ou só precisa de um pedacinho dele. Para isso, use serde_json::Value. Ele guarda qualquer JSON: um objeto, um array, uma string, um número, um bool ou null. A macro json! monta um Value com uma sintaxe parecida com JSON:
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "Lisboa", "temps": [18, 21, 19], "sunny": true });
println!("Cidade: {}", data["city"]); println!("Primeira temperatura: {}", data["temps"][0]);
// as_str() devolve um &str comum em vez de uma string JSON if let Some(city) = data["city"].as_str() { println!("Texto puro: {}", city); }
// uma chave ausente devolve Null em vez de travar println!("Vento: {}", data["wind"]);}Saída:
Cidade: "Lisboa"Primeira temperatura: 18Texto puro: LisboaVento: nullRepare que imprimir data["city"] mostra as aspas: ainda é uma string JSON. Use .as_str(), .as_i64() ou .as_bool() para obter valores comuns do Rust. Cada um devolve um Option, porque o valor pode ser de outro tipo.
O Value é flexível, mas você perde a ajuda do compilador. Prefira uma struct sempre que souber o formato.
Lendo e gravando um arquivo JSON
Seção intitulada “Lendo e gravando um arquivo JSON”Agora um exemplo do mundo real: salvar as configurações de um app em config.json e carregá-las de novo. Ele junta esta página com a de Arquivos em Rust. O std::fs::write salva o texto, e o std::fs::read_to_string lê de volta:
use serde::{Deserialize, Serialize};use std::error::Error;use std::fs;
#[derive(Serialize, Deserialize, Debug)]struct Config { username: String, theme: String, font_size: u32,}
fn main() -> Result<(), Box<dyn Error>> { let config = Config { username: String::from("ana"), theme: String::from("escuro"), font_size: 14, };
// salvar let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// carregar let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("Carregado: {:?}", loaded); Ok(())}Saída:
Carregado: Config { username: "ana", theme: "escuro", font_size: 14 }Por que Box<dyn Error>? Este main pode falhar de dois jeitos diferentes: com um erro de arquivo (std::io::Error) ou com um erro de JSON (serde_json::Error). Box<dyn Error> quer dizer “qualquer tipo de erro”, então o ? funciona para os dois. A página de Erros personalizados explica isso com mais detalhes.
Erros comuns
Seção intitulada “Erros comuns”Esquecer de derivar Deserialize. Se a sua struct só deriva Serialize, você consegue gerar JSON, mas não ler. O erro é longo, mas as linhas importantes são estas:
error[E0277]: the trait bound `User: serde::Deserialize<'de>` is not satisfied |10 | let user: User = serde_json::from_str(r#"{"name":"Ana","age":28}"#).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `User` typeO compilador diz que User não implementa a trait Deserialize e sugere acrescentar #[derive(serde::Deserialize)]. A solução: #[derive(Serialize, Deserialize)]. Se, em vez disso, aparecer “cannot find derive macro Serialize” (não foi possível encontrar a macro derive Serialize), você adicionou o serde sem a feature derive. Rode cargo add serde --features derive.
Usar .unwrap() em dados que você não controla. Em exemplos pequenos tudo bem, mas JSON vindo de um usuário ou de um site pode estar quebrado. Um arquivo ruim vai derrubar o seu programa com um panic. Use ? ou match, como mostrado acima.
Esperar que data["name"] seja impresso sem aspas. Um Value é impresso como JSON. Chame .as_str() para obter o texto puro.
Prática
Seção intitulada “Prática”1. Salve uma lista de compras
Seção intitulada “1. Salve uma lista de compras”Crie uma struct Item com um name (nome, String) e uma qty (quantidade, u32). Coloque dois itens em um Vec, transforme o vetor inteiro em JSON com uma única chamada e imprima.
Dica: só a struct precisa de #[derive(Serialize)]. Um Vec de coisas serializáveis também é serializável, e vira um array JSON.
Ver solução
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("maçãs"), qty: 3 }, Item { name: String::from("pão"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}Saída:
[{"name":"maçãs","qty":3},{"name":"pão","qty":1}]O serde já sabe lidar com Vec, String e números, então derivar Serialize em Item é tudo o que precisa. Experimente to_string_pretty para ver a mesma lista espalhada em várias linhas.
2. Configurações com valores padrão
Seção intitulada “2. Configurações com valores padrão”Leia este JSON para uma struct Settings: {"siteTitle": "Meu blog", "postsPerPage": 5}. A struct deve ter site_title, posts_per_page e um theme (tema) que pode estar ausente. Depois, leia também {"siteTitle": "Anotações"}, em que posts_per_page deve cair no valor padrão 0. Imprima os dois com {:?}.
Dica: você precisa de #[serde(rename_all = "camelCase")] na struct, Option<String> para theme e #[serde(default)] em posts_per_page.
Ver solução
use serde::Deserialize;
#[derive(Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct Settings { site_title: String, #[serde(default)] posts_per_page: u32, theme: Option<String>,}
fn main() { let full = r#"{"siteTitle": "Meu blog", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "Anotações"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Saída:
Settings { site_title: "Meu blog", posts_per_page: 5, theme: None }Settings { site_title: "Anotações", posts_per_page: 0, theme: None }O rename_all liga siteTitle a site_title e postsPerPage a posts_per_page. Nenhum dos dois JSON tem theme, então ele é None nas duas vezes, e o postsPerPage que falta no segundo vira 0. Tire o #[serde(default)] e o segundo unwrap() causa um panic com “missing field postsPerPage” (falta o campo postsPerPage). O .unwrap() não tem problema aqui porque nós mesmos escrevemos o JSON; para entradas de verdade, use ? ou match.
- Adicione o
serde(com--features derive) e oserde_json, e depois coloque#[derive(Serialize, Deserialize)]nas suas structs. serde_json::to_string/to_string_prettytransformam um valor em JSON;serde_json::from_strtransforma JSON em um valor.from_strdevolve umResult. Com dados reais, trate com?oumatch, e não com.unwrap().- Use
Optionou#[serde(default)]para chaves que podem faltar, erename/rename_allpara bater com os nomes do JSON. - Use
serde_json::Valueejson!quando não souber o formato. Quando souber, prefira structs.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Qual derive permite transformar texto JSON na sua struct?
2. Um objeto JSON pode não ter a chave bio. Qual é um bom tipo para esse campo?
3. A sua struct tem um campo display_name, mas o JSON usa displayName. Qual é a solução mais simples para a struct inteira?
4. O que o serde faz, por padrão, com as chaves do JSON que a sua struct não tem?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- serde.rs — o guia oficial do Serde (em inglês): uma visão geral de como o serde funciona, com exemplos para vários formatos de dados.
- Container attributes — serde.rs (em inglês): opções para uma struct ou um enum inteiro, como
rename_alledeny_unknown_fields. - Field attributes — serde.rs (em inglês): opções para campos individuais, como
rename,default,skipeskip_serializing_if. serde_json— docs.rs (em inglês): a página principal do crate, com exemplos de JSON com e sem tipo e da macrojson!.serde_json::Value— docs.rs (em inglês): todos os métodos para ler JSON sem tipo, comoas_str,as_i64eget.