JSON vers Rust : générer des structs serde (guide 2026)
Collez votre payload dans le convertisseur JSON vers Rust et copiez la struct serde qu’il génère. Voilà tout le flux de travail JSON vers struct Rust : rien à installer, rien n’est téléversé, tout se passe dans votre navigateur. Cela couvre le cas « il me faut une struct tout de suite » en quelques secondes.
Générer une struct et générer la bonne struct sont pourtant deux problèmes distincts. Un convertisseur ne peut que deviner. Et ici, Rust élève l’enjeu. En TypeScript, une mauvaise supposition se dégrade en un any permissif que vous pouvez ignorer. Avec serde, une mauvaise supposition échoue à la désérialisation : un flottant glissé dans un i64, un null inattendu dans un champ non optionnel, et serde_json::from_str vous rend un Err au lieu de vos données. Voilà pourquoi obtenir la bonne struct compte davantage en Rust que dans la plupart des langages.
Voici comment fonctionne réellement l’inférence, la règle de typage numérique que la plupart des convertisseurs traitent mal, quand recourir à #[serde(rename)] plutôt qu’à #[serde(rename_all)], et comment gérer les dates, les clés dynamiques et les mots-clés Rust pour que la sortie compile toujours.
Comment convertir du JSON en Rust
Convertir du JSON en Rust se fait en trois étapes :
- Collez votre JSON. Déposez un objet, un tableau ou une réponse d’API brute dans la zone de saisie. La conversion s’exécute instantanément et entièrement côté client.
- Ajustez la sortie. Renommez la struct racine, basculez les dérivations serde, ajoutez
DebugetClone, ou retirez la visibilitépubpour correspondre au style de votre crate. - Copiez ou téléchargez. Récupérez le Rust généré en un clic et collez-le directement dans votre projet.
Le parcours transactionnel tient en ces trois étapes. Si votre entrée est minifiée ou si vous n’êtes pas sûr qu’elle soit valide, passez-la d’abord par un formateur JSON pour que le convertisseur dispose d’un JSON propre et bien formé. Le reste de ce guide explique comment lire la sortie afin de corriger les cas qu’un outil ne peut pas inférer tout seul.
Comment les types JSON se mappent vers Rust
Chaque valeur JSON a un équivalent Rust, mais le mappage n’est pas tout à fait bijectif :
| Valeur JSON | Type Rust |
|---|---|
"text" | String |
42 | i64 (grandes valeurs u64, au-delà f64) |
3.14, 2e3 | f64 |
true / false | bool |
null | Option<T> (ou Option<serde_json::Value>) |
[1, 2, 3] | Vec<T> |
{ ... } | une struct nommée |
Les objets, c’est là que le travail se fait. Prenez un payload REST typique :
{
"id": 101,
"name": "Ada Lovelace",
"email": "ada@example.com",
"active": true,
"roles": ["admin", "user"]
}
Le convertisseur produit une struct prête pour 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>,
}
Chaque scalaire correspond à une primitive, et roles devient Vec<String>. Le seul type JSON qui ne se mappe pas proprement est number : JSON possède un unique type numérique, alors que Rust vous force à choisir entre i64, u64 et f64. Ce choix fait l’objet d’une section entière ci-dessous, car c’est là que la plupart des générateurs se trompent.
Comment le convertisseur infère les structs
Quatre règles couvrent presque tout ce que vous lui donnerez : une struct par forme d’objet, une fusion clé par clé pour les tableaux, un typage numérique précis et des noms de champs idiomatiques.
Inférence structurelle : une struct nommée par objet
Chaque forme d’objet distincte devient sa propre struct nommée. Les objets imbriqués ne sont pas incorporés en ligne ; ils sont hissés dans des définitions séparées et référencées :
{ "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,
}
L’owner imbriqué devient sa propre struct Owner, référencée par champ. Les formes identiques sont dédupliquées, de sorte que deux champs de même structure partagent une seule struct au lieu de produire des copies.
Fusion des tableaux et champs Option
Lorsque vous passez un tableau d’objets, le convertisseur les fusionne clé par clé. Une clé présente dans certains éléments mais pas dans d’autres devient un 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 apparaît dans chaque élément, il reste donc obligatoire. nick est absent du second utilisateur, il devient donc Option<String>. Notez ce qui n’est pas là : aucun #[serde(default)]. serde traite déjà Option comme optionnel et désérialise une clé manquante en None ; l’attribut serait redondant.
Typage numérique correct : i64, u64, f64
Voici la règle que les outils concurrents esquivent, et c’est celle qui casse sur les payloads réels. Le générateur applique trois paliers. Un entier se mappe vers i64. Si une valeur dépasse i64::MAX, elle est promue en u64. Au-delà de u64::MAX, elle retombe sur f64. Tout jeton écrit avec un point décimal ou un exposant, comme 1.0 ou 2e3, se mappe vers f64 quelle que soit sa 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,
}
Ici, user_id est plus grand que i64::MAX, il atterrit donc dans u64 et fait toujours l’aller-retour. balance porte un point décimal, c’est donc un f64, pas un entier. Cela importe car serde impose le type à la désérialisation. Donnez-lui une struct qui type un identifiant façon snowflake ou Twitter en i32 et la valeur déborde ; typez un champ flottant en i64 et serde renvoie une erreur plutôt que de tronquer silencieusement. La règle des trois paliers est ce qui permet à une rust struct from json générée de continuer à se désérialiser au lieu de paniquer au premier grand identifiant.
Champs snake_case et #[serde(rename)]
Les clés JSON sont souvent en camelCase ; les champs Rust idiomatiques sont en snake_case. Le convertisseur renomme le champ et ajoute un #[serde(rename)] qui le remappe vers la clé JSON exacte :
{ "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,
}
Une clé déjà en snake_case, comme login, ne reçoit aucun rename. Les autres se remappent proprement vers le format réseau tandis que votre code Rust se lit naturellement.
#[serde(rename)] vs #[serde(rename_all)]
L’outil émet un #[serde(rename)] par champ parce que cela fonctionne toujours, même quand un payload mélange camelCase, snake_case et clés irrégulières dans le même objet. Il n’a jamais à raisonner sur une convention partagée qui pourrait ne pas exister.
Quand chaque champ d’une struct partage effectivement une seule convention, vous pouvez réduire ces attributs en un unique rename au niveau du conteneur. Supprimez les lignes par champ et placez un seul #[serde(rename_all = "camelCase")] sur la 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,
}
Les deux versions désérialisent le même JSON. Utilisez rename par champ pour les clés mixtes ou irrégulières ; recourez à rename_all quand une struct est uniforme et que vous voulez moins de bruit. serde livre aussi d’autres conventions comme "snake_case", "kebab-case" et "SCREAMING_SNAKE_CASE" pour le même attribut.
Mots-clés Rust et clés non identifiantes
Les clés JSON ne sont que des chaînes, rien n’empêche donc une API d’envoyer une clé nommée type ou match, deux mots réservés en Rust. Le générateur les assainit en un identifiant légal plus un 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,
}
La forme à underscore final (type_) est délibérément choisie plutôt qu’un identifiant brut comme r#type. Les identifiants bruts ne peuvent pas exprimer tous les mots-clés dans toutes les positions ; self, crate et super en particulier sont rejetés comme identifiants bruts, une approche assainir-puis-renommer est donc la seule qui compile toujours. Les clés non identifiantes comme first-name ou un 2fa commençant par un chiffre reçoivent le même traitement : un nom Rust valide et un rename vers la clé JSON littérale.
Désérialiser avec serde_json
Une fois la struct obtenue, ajoutez les dépendances à Cargo.toml :
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Ensuite, une seule ligne suffit pour parser. Cet exemple est complet et compile tel quel :
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(())
}
La dérivation Deserialize fait tout le travail. Utilisez serde_json::from_str pour une chaîne, from_slice pour une tranche d’octets, ou from_reader pour un fichier ou un corps HTTP, et serde_json::to_string pour resérialiser une valeur en JSON. C’est la récompense d’un typage correct : le serde derive généré fait office de validateur à l’exécution. Un appel de désérialisation serde_json qui renvoie Ok garantit alors que les données correspondaient vraiment à votre struct, pas seulement que le compilateur le supposait.
Dates, clés dynamiques et champs inconnus
Certaines formes ne se mappent pas vers un simple champ de struct, et le générateur leur attribue une valeur par défaut raisonnable que vous resserrez ensuite à la main.
Dates. JSON n’a pas de type date, un horodatage ISO ou RFC 3339 arrive donc comme une simple String. Pour une vraie gestion des dates, changez le champ vers un type chrono et activez la fonctionnalité serde de chrono (chrono = { version = "0.4", features = ["serde"] }). serde parse alors automatiquement le RFC 3339 :
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub name: String,
pub created_at: DateTime<Utc>,
}
Clés dynamiques. Quand les clés varient à l’exécution, comme une map d’identifiants vers des valeurs, une struct fixe est la mauvaise forme. Utilisez une HashMap indexée par String :
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub scores: HashMap<String, i64>,
}
Champs supplémentaires inconnus. Pour conserver une struct typée tout en capturant les clés que vous n’avez pas modélisées, ajoutez un champ #[serde(flatten)] adossé à une HashMap. Pour une valeur véritablement dynamique, serde_json::Value est le fourre-tout. Quand votre vrai besoin est de valider une structure plutôt que de générer des types, le guide de validation JSON Schema couvre l’application d’un contrat de bout en bout.
json-to-rust vs quicktype vs typeshare vs manuel
Il n’existe pas de meilleure façon unique de produire des types Rust à partir de JSON. Cela dépend de l’endroit où vit le JSON et du sens de la conversion.
| Approche | Idéal pour | Note |
|---|---|---|
| Convertisseur en ligne (cet outil, transform.tools) | Conversions ponctuelles, payloads sensibles, zéro installation | Infère depuis un échantillon, entièrement côté client |
| quicktype | Sortie multi-langage, génération de code en pipeline | Aussi piloté par échantillon ; CLI ou web |
| typeshare | Partager des types Rust existants avec TS, Swift, Kotlin | Sens inverse : Rust vers d’autres langages |
| schemafy / typify | Générer à partir d’un JSON Schema | L’entrée est un schéma, pas un échantillon |
| Écrire les structs à la main | Petits payloads, apprendre serde | Contrôle total, fastidieux et sujet aux erreurs à grande échelle |
La distinction à intérioriser : un convertisseur en ligne et quicktype infèrent tous deux les types à partir d’un échantillon de JSON, tandis que typeshare fonctionne dans l’autre sens, en transformant des types Rust annotés en TypeScript ou Swift. schemafy et typify prennent un JSON Schema en entrée, pas des données d’exemple. Si votre base de code est en TypeScript plutôt qu’en Rust, la même approche pilotée par échantillon s’applique avec le convertisseur JSON vers TypeScript, et le guide des interfaces JSON vers TypeScript couvre ce versant en profondeur.
Pièges courants lors de la génération de Rust à partir de JSON
Les structs générées sont un point de départ. Surveillez ceci avant de faire confiance à la sortie sur des données réelles.
- Un seul échantillon révèle rarement toutes les formes. Les champs optionnels ne peuvent être inférés qu’à partir de plusieurs éléments de tableau. Collez un tableau représentatif pour que l’inférence des
Optionsoit exacte plutôt qu’une supposition tirée d’un seul objet chanceux. - Les tableaux vides ou mixtes retombent sur
serde_json::Value. C’est la réponse honnête quand il n’y a rien à inférer. Fournissez un échantillon plus riche pour obtenir un type d’élément concret. - Ne réduisez pas un grand identifiant à
i32. Les identifiants snowflake et similaires dépassent 2^53 et débordent un champ 32 bits. Conservez lei64ouu64généré. - Un champ toujours
nulldevientOption<serde_json::Value>, pas un simple optionnel. L’outil n’a aucun type à inférer, traitez-le donc comme un espace réservé et donnez-lui un vrai type dès que vous en connaissez un. serde_json::Valuerequiert la dépendanceserde_json. Si la sortie contientValueet que votreCargo.tomln’a pas la crate, le code ne compilera pas.- Une date laissée en
Stringne fera pas de calculs de dates. Si vous prévoyez de comparer ou de formater des horodatages, passez à un type chrono au lieu de parser des chaînes à la main.
Foire aux questions
Pourquoi serde rejette-t-il mon JSON alors que la struct semble correcte ?
Presque toujours une incohérence de type. Les deux causes habituelles sont une valeur à virgule flottante qui atterrit dans un champ i64, et une clé parfois absente mais non typée en Option. serde impose le type à la désérialisation, corrigez donc le champ en f64 ou Option<T>, ou régénérez la struct à partir d’un échantillon représentatif.
Quel type numérique Rust un entier JSON doit-il devenir ?
Par défaut, i64. Promouvez en u64 quand une valeur dépasse i64::MAX, et retombez sur f64 au-delà de u64::MAX. Tout nombre écrit avec un point décimal ou un exposant se mappe vers f64 quelle que soit sa taille, car serde rejette un flottant désérialisé dans un champ entier.
Comment rendre un champ optionnel quand le JSON l’omet ?
Typez-le en Option<T>. serde traite Option comme optionnel automatiquement et désérialise une clé manquante en None, vous n’avez donc pas besoin de #[serde(default)]. Le convertisseur marque un champ Option quand certains éléments échantillonnés n’ont pas la clé.
Faut-il utiliser #[serde(rename)] ou #[serde(rename_all)] ?
Utilisez #[serde(rename)] par champ quand un payload mélange les styles de nommage ; cela fonctionne toujours. Si chaque champ d’une struct suit une seule convention, supprimez les attributs par champ et placez plutôt un unique #[serde(rename_all = "camelCase")] sur la struct. Les deux désérialisent à l’identique.
Comment gérer les clés JSON qui sont des mots-clés Rust comme type ?
Le générateur émet type_ avec un mappage #[serde(rename = "type")] vers la clé d’origine. C’est plus robuste qu’un identifiant brut comme r#type, car les identifiants bruts ne peuvent pas couvrir self, crate et super dans toutes les positions.
Pourquoi les dates JSON sont-elles typées en String plutôt qu’en type date ?
JSON n’a pas de type date, un horodatage n’est donc qu’une chaîne sur le réseau, et String est le choix par défaut honnête. Pour une vraie gestion des dates, changez le champ vers le DateTime<Utc> ou le NaiveDate de chrono et activez la fonctionnalité serde de chrono ; serde parse alors le RFC 3339 pour vous.
Comment désérialiser du JSON dans la struct générée ?
Ajoutez serde_json à Cargo.toml, puis écrivez let root: Root = serde_json::from_str(json)?;. La dérivation Deserialize fait le reste. Utilisez from_slice pour une tranche d’octets et from_reader pour un fichier ou un corps HTTP.
Mon JSON reste-t-il privé avec un convertisseur JSON vers struct Rust en ligne ?
Oui. La conversion s’exécute à 100 % dans votre navigateur avec JavaScript. Votre JSON, y compris les jetons, les identifiants et les données clients, ne quitte jamais la page et n’est jamais envoyé à un serveur. Quand vous êtes prêt, essayez le convertisseur JSON vers Rust sur votre propre payload.