JSON para struct Rust: gerar structs serde (guia 2026)
Cole seu payload no conversor JSON para struct Rust e copie a struct serde que ele gera. Esse é todo o fluxo de JSON para struct Rust: nada para instalar, nada enviado para servidores, tudo feito no seu navegador. Ele resolve o caso “preciso de uma struct agora” em segundos.
Gerar uma struct e gerar a struct certa, porém, são problemas diferentes. Um conversor só pode adivinhar. E aqui o Rust eleva a aposta. Em TypeScript, um palpite errado degrada para um any frouxo que você pode ignorar. Com serde, um palpite errado falha na desserialização: um float caído dentro de um i64, um null inesperado num campo não opcional, e serde_json::from_str te entrega um Err em vez dos seus dados. É por isso que acertar a struct importa mais em Rust do que na maioria das linguagens.
Veja como a inferência realmente funciona, a regra de tipagem numérica que a maioria dos conversores erra, quando recorrer a #[serde(rename)] em vez de #[serde(rename_all)], e como lidar com datas, chaves dinâmicas e palavras reservadas do Rust para que a saída sempre compile.
Como converter JSON para Rust
Converter JSON para Rust leva três passos:
- Cole seu JSON. Solte um objeto, um array ou uma resposta bruta de API na caixa de entrada. A conversão roda instantaneamente e inteiramente no lado do cliente.
- Ajuste a saída. Renomeie a struct raiz, alterne os derives de serde, adicione
DebugeClone, ou remova a visibilidadepubpara combinar com o estilo do seu crate. - Copie ou baixe. Pegue o Rust gerado com um clique e cole direto no seu projeto.
Esse é todo o caminho transacional. Se sua entrada está minificada ou você não tem certeza de que é válida, passe-a antes por um formatador JSON para que o conversor trabalhe com um JSON limpo e bem formado. O resto deste guia explica como ler a saída para você corrigir os casos que uma ferramenta não consegue inferir sozinha.
Como os tipos JSON mapeiam para Rust
Todo valor JSON tem um correspondente em Rust, mas o mapeamento não é bem um-para-um:
| Valor JSON | Tipo Rust |
|---|---|
"text" | String |
42 | i64 (valores grandes u64, além disso f64) |
3.14, 2e3 | f64 |
true / false | bool |
null | Option<T> (ou Option<serde_json::Value>) |
[1, 2, 3] | Vec<T> |
{ ... } | uma struct nomeada |
Os objetos são onde o trabalho acontece. Pegue um payload REST típico:
{
"id": 101,
"name": "Ada Lovelace",
"email": "ada@example.com",
"active": true,
"roles": ["admin", "user"]
}
O conversor produz uma struct pronta para serde:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
Cada escalar mapeia para um primitivo, e roles vira Vec<String>. O único tipo JSON que não mapeia de forma limpa é o number: o JSON tem um único tipo numérico, enquanto o Rust obriga você a escolher entre i64, u64 e f64. Essa escolha ganha uma seção só dela mais abaixo, porque é onde a maioria dos geradores erra.
Como o conversor infere as structs
Quatro regras cobrem quase tudo o que você vai passar para ele: uma struct por formato de objeto, mesclagem chave a chave para arrays, tipagem numérica precisa e nomes de campo idiomáticos.
Inferência estrutural: uma struct nomeada por objeto
Cada formato de objeto distinto vira sua própria struct nomeada. Objetos aninhados não são embutidos; o conversor os eleva a definições separadas e os referencia:
{ "repo": "serde", "owner": { "login": "dtolnay", "id": 100 } }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub repo: String,
pub owner: Owner,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
pub login: String,
pub id: i64,
}
O owner aninhado vira sua própria struct Owner, referenciada pelo campo. Formatos idênticos são deduplicados, então dois campos com a mesma estrutura compartilham uma única struct em vez de gerar cópias.
Mesclagem de arrays e campos Option
Quando você passa um array de objetos, o conversor os mescla chave a chave. Uma chave presente em alguns elementos mas não em outros vira um Option:
{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub users: Vec<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: i64,
pub nick: Option<String>,
}
id aparece em todos os elementos, então continua obrigatório. nick está ausente do segundo usuário, então vira Option<String>. Note o que não está aqui: nenhum #[serde(default)]. O serde já trata Option como opcional e desserializa uma chave ausente para None, então o atributo seria redundante.
Tipagem numérica correta: i64, u64, f64
Esta é a regra que as ferramentas concorrentes pulam, e é a que quebra em payloads reais. O gerador aplica três níveis. Um inteiro mapeia para i64. Se um valor exceder i64::MAX, é promovido para u64. Além de u64::MAX, cai para f64. Qualquer token escrito com ponto decimal ou expoente, como 1.0 ou 2e3, mapeia para f64 independentemente da magnitude.
{ "user_id": 12000000000000000000, "balance": 19.99, "retries": 3 }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub user_id: u64,
pub balance: f64,
pub retries: i64,
}
Aqui user_id é maior que i64::MAX, então cai em u64 e ainda faz o round-trip. balance traz um ponto decimal, então é f64, não um inteiro. Isso importa porque o serde impõe o tipo na desserialização. Dê a ele uma struct que tipa um snowflake ou um ID no estilo Twitter como i32 e o valor estoura; tipe um campo float como i64 e o serde retorna um erro em vez de truncar silenciosamente. A regra de três níveis é o que mantém uma rust struct from json gerada desserializando em vez de entrar em pânico no primeiro ID grande.
Campos snake_case e #[serde(rename)]
As chaves JSON são frequentemente camelCase; os campos idiomáticos do Rust são snake_case. O conversor renomeia o campo e adiciona um #[serde(rename)] que o mapeia de volta para a chave JSON exata:
{ "login": "octocat", "publicRepos": 15, "followerCount": 9001, "createdAt": "2011-01-25" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub login: String,
#[serde(rename = "publicRepos")]
pub public_repos: i64,
#[serde(rename = "followerCount")]
pub follower_count: i64,
#[serde(rename = "createdAt")]
pub created_at: String,
}
Uma chave que já é snake_case, como login, não recebe rename. As outras mapeiam de volta de forma limpa para o formato de transmissão enquanto seu código Rust se lê naturalmente.
#[serde(rename)] vs #[serde(rename_all)]
A ferramenta emite um #[serde(rename)] por campo porque ele sempre funciona, mesmo quando um payload mistura camelCase, snake_case e chaves irregulares no mesmo objeto. Ela nunca precisa raciocinar sobre uma convenção compartilhada que pode não existir.
Quando todos os campos de uma struct de fato compartilham uma convenção, você pode colapsar esses atributos em um único rename no nível do contêiner. Apague as linhas por campo e coloque um único #[serde(rename_all = "camelCase")] na struct:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub login: String,
pub public_repos: i64,
pub follower_count: i64,
pub created_at: String,
}
Ambas as versões desserializam o mesmo JSON. Use rename por campo para chaves mistas ou irregulares; recorra a rename_all quando uma struct é uniforme e você quer menos ruído. O serde também traz outras convenções como "snake_case", "kebab-case" e "SCREAMING_SNAKE_CASE" para o mesmo atributo.
Palavras reservadas do Rust e chaves que não são identificadores
As chaves JSON são apenas strings, então nada impede uma API de enviar uma chave chamada type ou match, ambas palavras reservadas em Rust. O gerador as saneia para um identificador válido mais um rename:
{ "type": "user", "match": true, "first-name": "Ada" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
#[serde(rename = "type")]
pub type_: String,
#[serde(rename = "match")]
pub match_: bool,
#[serde(rename = "first-name")]
pub first_name: String,
}
A forma com underscore ao final (type_) é escolhida deliberadamente em vez de um identificador bruto como r#type. Identificadores brutos não conseguem expressar toda palavra reservada em toda posição; self, crate e super em particular são rejeitados como identificadores brutos, então uma abordagem de sanear-mais-renomear é a única que sempre compila. Chaves que não são identificadores, como first-name ou um 2fa começando com dígito, recebem o mesmo tratamento: um nome Rust válido e um rename de volta para a chave JSON literal.
Desserializando com serde_json
Uma vez que você tenha a struct, adicione as dependências ao Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Depois é uma única linha para fazer o parse. Este exemplo é completo e compila como está:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"{"id":101,"name":"Ada Lovelace","email":"a@ex.com","active":true,"roles":["admin"]}"#;
let root: Root = serde_json::from_str(data)?;
println!("{} has {} role(s)", root.name, root.roles.len());
Ok(())
}
O derive Deserialize faz todo o trabalho. Use serde_json::from_str para uma string, from_slice para uma fatia de bytes, ou from_reader para um arquivo ou corpo HTTP, e serde_json::to_string para serializar um valor de volta para JSON. Essa é a recompensa por acertar os tipos: o serde derive que você gerou é um validador em tempo de execução, então uma chamada serde_json de desserialização que retorna Ok é uma garantia de que os dados combinaram com sua struct, não apenas a suposição do compilador de que combinaram.
Datas, chaves dinâmicas e campos desconhecidos
Alguns formatos não mapeiam para um campo simples de struct, e o gerador lhes dá um padrão sensato que você depois aperta à mão.
Datas. O JSON não tem tipo de data, então um timestamp ISO ou RFC 3339 chega como uma String simples. Para trabalhar de verdade com datas, troque o campo por um tipo do chrono e habilite a feature serde do chrono (chrono = { version = "0.4", features = ["serde"] }). O serde então faz o parse de RFC 3339 automaticamente:
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub name: String,
pub created_at: DateTime<Utc>,
}
Chaves dinâmicas. Quando as chaves variam em tempo de execução, como um mapa de IDs para valores, uma struct fixa é o formato errado. Use um HashMap com chave String:
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub scores: HashMap<String, i64>,
}
Campos extras desconhecidos. Para manter uma struct tipada mas ainda capturar chaves que você não modelou, adicione um campo #[serde(flatten)] respaldado por um HashMap. Para um valor que é genuinamente dinâmico, serde_json::Value é o pega-tudo. Quando sua necessidade real é validar a estrutura em vez de gerar tipos, o guia de validação com JSON Schema mostra como impor um contrato de ponta a ponta.
json-to-rust vs quicktype vs typeshare vs manual
Não existe uma única melhor forma de produzir tipos Rust a partir de JSON. Depende de onde o JSON vive e em qual direção você está convertendo.
| Abordagem | Melhor para | Nota |
|---|---|---|
| Conversor online (esta ferramenta, transform.tools) | Conversões pontuais, payloads sensíveis, zero instalação | Infere a partir de uma amostra, totalmente no cliente |
| quicktype | Saída multilinguagem, codegen em pipeline | Também guiado por amostra; CLI ou web |
| typeshare | Compartilhar tipos Rust existentes com TS, Swift, Kotlin | Direção oposta: de Rust para outras linguagens |
| schemafy / typify | Gerar a partir de um JSON Schema | A entrada é um schema, não uma amostra |
| Escrever structs à mão | Payloads minúsculos, aprender serde | Controle total, tedioso e propenso a erros em escala |
A diferença que vale guardar: um conversor online e o quicktype ambos inferem tipos a partir de uma amostra de JSON, enquanto o typeshare corre no sentido inverso, transformando tipos Rust anotados em TypeScript ou Swift. schemafy e typify recebem um JSON Schema como entrada, não dados de exemplo. Se sua base de código é TypeScript em vez de Rust, a mesma abordagem guiada por amostra se aplica com o conversor JSON para TypeScript, e o guia de interfaces JSON para TypeScript cobre esse lado em profundidade.
Ciladas comuns ao gerar Rust a partir de JSON
Structs geradas são um ponto de partida. Fique atento a estas antes de confiar na saída com dados ao vivo.
- Uma única amostra raramente revela todos os formatos. Campos opcionais só podem ser inferidos a partir de múltiplos elementos de array. Cole um array representativo para que a inferência de
Optionseja precisa em vez de um palpite tirado de um objeto sortudo. - Arrays vazios ou mistos caem para
serde_json::Value. Essa é a resposta honesta quando não há nada de onde inferir. Alimente uma amostra mais rica para obter um tipo de elemento concreto. - Não estreite um ID grande para
i32. IDs snowflake e semelhantes excedem 2^53 e estouram um campo de 32 bits. Mantenha oi64ouu64gerado. - Um campo sempre-
nullviraOption<serde_json::Value>, não um opcional simples. A ferramenta não tem tipo a inferir, então trate-o como um placeholder e dê a ele um tipo real quando você souber qual. serde_json::Valueprecisa da dependênciaserde_json. Se a saída contémValuee seuCargo.tomlnão tem o crate, o código não vai compilar.- Uma data deixada como
Stringnão fará contas de data. Se você planeja comparar ou formatar timestamps, troque para um tipo do chrono em vez de fazer parse de strings à mão.
Perguntas frequentes
Por que o serde rejeita meu JSON mesmo quando a struct parece certa?
Quase sempre uma incompatibilidade de tipo. As duas causas usuais são um valor de ponto flutuante caindo num campo i64 e uma chave que às vezes está ausente mas não é tipada como Option. O serde impõe o tipo na desserialização, então corrija o campo para f64 ou Option<T>, ou regenere a struct a partir de uma amostra representativa.
Que tipo numérico do Rust um inteiro JSON deve virar?
O padrão é i64. Promova para u64 quando um valor exceder i64::MAX, e caia para f64 além de u64::MAX. Qualquer número escrito com ponto decimal ou expoente mapeia para f64 independentemente do tamanho, porque o serde rejeita um float desserializado num campo inteiro.
Como faço um campo ser opcional quando o JSON o omite?
Tipe-o como Option<T>. O serde trata Option como opcional automaticamente e desserializa uma chave ausente para None, então você não precisa de #[serde(default)]. O conversor marca um campo como Option quando alguns itens amostrados não têm a chave.
Devo usar #[serde(rename)] ou #[serde(rename_all)]?
Use #[serde(rename)] por campo quando um payload mistura estilos de nomenclatura; ele sempre funciona. Se todos os campos de uma struct seguem uma convenção, apague os atributos por campo e coloque um único #[serde(rename_all = "camelCase")] na struct. Ambos desserializam de forma idêntica.
Como lido com chaves JSON que são palavras reservadas do Rust, como type?
O gerador emite type_ com um mapeamento #[serde(rename = "type")] de volta para a chave original. Isso é mais robusto do que um identificador bruto como r#type, porque identificadores brutos não conseguem cobrir self, crate e super em toda posição.
Por que as datas JSON são tipadas como String em vez de um tipo de data?
O JSON não tem tipo de data, então um timestamp é apenas uma string no formato de transmissão, e String é o padrão honesto. Para lidar de fato com datas, mude o campo para o DateTime<Utc> ou NaiveDate do chrono e habilite a feature serde do chrono; o serde então faz o parse de RFC 3339 para você.
Como desserializo JSON na struct gerada?
Adicione serde_json ao Cargo.toml, depois escreva let root: Root = serde_json::from_str(json)?;. O derive Deserialize cuida do resto. Use from_slice para uma fatia de bytes e from_reader para um arquivo ou corpo HTTP.
Meu JSON fica privado ao usar um conversor online de JSON para struct Rust?
Sim. A conversão roda 100% no seu navegador com JavaScript. Seu JSON, incluindo tokens, IDs e dados de clientes, nunca sai da página e nunca é enviado a um servidor. Quando estiver pronto, experimente o conversor JSON para Rust com seu próprio payload.