Pular para o conteúdo

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.

Dentro do seu projeto, adicione os dois crates. A feature derive faz o serde escrever o código de tradução para você:

Terminal window
cargo add serde --features derive
cargo add serde_json

Agora 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.

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
}
▶ Experimente você mesmo

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.

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 anos
▶ Experimente você mesmo

Repare em duas coisas:

  • r#"..."# é uma raw string (string bruta). Dentro dela, você pode escrever " à vontade, o que ajuda muito com JSON.
  • from_str devolve um Result, porque o texto pode não ser um JSON válido. Aqui o main devolve Result, então podemos usar ? para repassar qualquer erro adiante.

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 15
Erro: invalid type: string "old", expected u32 at line 1 column 27
▶ Experimente você mesmo

As 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.

E se um campo for mesmo opcional? Você tem duas boas opções:

  • Fazer do campo um Option. Uma chave ausente (ou null) vira None.
  • Acrescentar #[serde(default)]. Uma chave ausente recebe o valor padrão do tipo: 0, false, uma String vazia ou um Vec vazio.
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: [] }
▶ Experimente você mesmo

Use Option quando “sem valor” significa algo diferente de “zero” ou “vazio”. Use default quando o valor padrão já serve.

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"}
▶ Experimente você mesmo

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.

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 API
2 tarefas restantes
▶ Experimente você mesmo

À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: 18
Texto puro: Lisboa
Vento: null
▶ Experimente você mesmo

Repare 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.

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 }
▶ Experimente você mesmo

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.

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` type

O 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.

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}]
▶ Experimente você mesmo

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.

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 }
▶ Experimente você mesmo

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 o serde_json, e depois coloque #[derive(Serialize, Deserialize)] nas suas structs.
  • serde_json::to_string / to_string_pretty transformam um valor em JSON; serde_json::from_str transforma JSON em um valor.
  • from_str devolve um Result. Com dados reais, trate com ? ou match, e não com .unwrap().
  • Use Option ou #[serde(default)] para chaves que podem faltar, e rename / rename_all para bater com os nomes do JSON.
  • Use serde_json::Value e json! quando não souber o formato. Quando souber, prefira structs.

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