Skip to content
Torna al blog
Tutorial

Da JSON a struct Rust: generare struct serde (guida 2026)

Converti JSON in struct Rust online: tipi numerici (i64/u64/f64), Option per i null, serde rename per il camelCase e le insidie da evitare. Gratis.

11 min di lettura

Da JSON a struct Rust: generare struct serde (guida 2026)

Incolla il payload nel convertitore da JSON a struct Rust e copia la struct serde che genera. Questo è l’intero flusso di lavoro da JSON a struct Rust: niente da installare, niente da caricare, tutto nel browser. Copre il caso “mi serve una struct subito” in pochi secondi.

Generare una struct e generare la struct giusta sono però problemi diversi. Un convertitore può solo tirare a indovinare. E qui Rust alza la posta. In TypeScript un’ipotesi sbagliata degrada in un any permissivo che puoi ignorare. Con serde, un’ipotesi sbagliata fallisce in fase di deserializzazione: un float finito in un i64, un null inatteso in un campo non opzionale, e serde_json::from_str ti restituisce un Err invece dei tuoi dati. Ecco perché azzeccare la struct conta più in Rust che nella maggior parte dei linguaggi.

Qui sotto trovi come funziona davvero l’inferenza, la regola sui tipi numerici che la maggior parte dei convertitori sbaglia, quando ricorrere a #[serde(rename)] rispetto a #[serde(rename_all)], e come gestire date, chiavi dinamiche e parole chiave di Rust perché l’output compili sempre.

Come convertire JSON in Rust

Convertire JSON in Rust richiede tre passaggi:

  1. Incolla il JSON. Trascina un oggetto, un array o una risposta grezza di un’API nella casella di input. La conversione avviene all’istante e interamente lato client.
  2. Regola l’output. Rinomina la struct radice, attiva o disattiva i derive di serde, aggiungi Debug e Clone, oppure togli la visibilità pub per adeguarti allo stile del tuo crate.
  3. Copia o scarica. Prendi il Rust generato con un clic e incollalo direttamente nel tuo progetto.

Ecco l’intero percorso transazionale. Se l’input è minificato o non sei sicuro che sia valido, passalo prima da un formattatore JSON così il convertitore lavora su JSON pulito e ben formato. Il resto della guida spiega come leggere l’output per correggere i casi che uno strumento non può inferire da solo.

Come i tipi JSON si mappano su Rust

Ogni valore JSON ha una controparte in Rust, ma la mappatura non è proprio uno a uno:

Valore JSONTipo Rust
"text"String
42i64 (valori grandi u64, oltre f64)
3.14, 2e3f64
true / falsebool
nullOption<T> (oppure Option<serde_json::Value>)
[1, 2, 3]Vec<T>
{ ... }una struct con nome

È con gli oggetti che si fa il grosso del lavoro. Prendi un tipico payload REST:

{
  "id": 101,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "active": true,
  "roles": ["admin", "user"]
}

Il convertitore produce una struct pronta per 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>,
}

Ogni scalare si mappa su un primitivo, e roles diventa Vec<String>. L’unico tipo JSON che non si mappa in modo pulito è number: JSON ha un solo tipo numerico, mentre Rust ti obbliga a scegliere tra i64, u64 e f64. A questa scelta è dedicata un’intera sezione più avanti, perché è lì che la maggior parte dei generatori sbaglia.

Come il convertitore inferisce le struct

Quattro regole coprono quasi tutto ciò che gli darai in pasto: una struct per ogni forma di oggetto, fusione chiave per chiave degli array, tipizzazione numerica precisa e nomi di campo idiomatici.

Inferenza strutturale: una struct con nome per ogni oggetto

Ogni forma distinta di oggetto diventa una struct con nome a sé. Gli oggetti annidati non vengono incorporati inline; vengono estratti in definizioni separate e referenziate:

{ "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 annidato diventa la sua struct Owner, referenziata dal campo. Le forme identiche vengono deduplicate, così due campi con la stessa struttura condividono un’unica struct invece di produrne copie.

Fusione degli array e campi Option

Quando passi un array di oggetti, il convertitore li fonde chiave per chiave. Una chiave presente in alcuni elementi ma non in altri diventa 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 compare in ogni elemento, quindi resta obbligatorio. nick manca nel secondo utente, quindi diventa Option<String>. Nota ciò che non c’è: nessun #[serde(default)]. serde tratta già Option come opzionale e deserializza una chiave mancante come None, quindi l’attributo sarebbe ridondante.

Tipizzazione numerica corretta: i64, u64, f64

Questa è la regola che gli strumenti concorrenti saltano, ed è quella che si rompe sui payload reali. Il generatore applica tre livelli. Un intero si mappa su i64. Se un valore supera i64::MAX, viene promosso a u64. Oltre u64::MAX, ripiega su f64. Qualsiasi token scritto con un punto decimale o un esponente, come 1.0 o 2e3, si mappa su f64 a prescindere dalla grandezza.

{ "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,
}

Qui user_id è più grande di i64::MAX, quindi finisce in u64 e continua a fare il round-trip. balance porta un punto decimale, quindi è f64, non un intero. Questo conta perché serde impone il tipo in fase di deserializzazione. Dagli una struct che tipizza uno snowflake o un ID in stile Twitter come i32 e il valore va in overflow; tipizza un campo float come i64 e serde restituisce un errore invece di troncare silenziosamente. È la regola a tre livelli a far sì che una struct Rust generata da JSON riesca a deserializzare invece di andare in panic al primo ID grande.

Campi snake_case e #[serde(rename)]

Le chiavi JSON sono spesso in camelCase; i campi Rust idiomatici sono in snake_case. Il convertitore rinomina il campo e aggiunge un #[serde(rename)] che lo rimappa esattamente sulla chiave JSON:

{ "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,
}

Una chiave già in snake_case, come login, non riceve alcun rename. Le altre si rimappano con precisione sul formato di trasmissione, mentre il tuo codice Rust si legge in modo naturale.

#[serde(rename)] vs #[serde(rename_all)]

Lo strumento emette un #[serde(rename)] per ogni campo perché funziona sempre, anche quando un payload mescola camelCase, snake_case e chiavi irregolari nello stesso oggetto. Non deve mai ragionare su una convenzione condivisa che potrebbe non esistere.

Quando tutti i campi di una struct condividono davvero una sola convenzione, puoi comprimere quegli attributi in un unico rename a livello di container. Elimina le righe per singolo campo e metti un solo #[serde(rename_all = "camelCase")] sulla 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,
}

Entrambe le versioni deserializzano lo stesso JSON. Usa rename per singolo campo con chiavi miste o irregolari; ricorri a rename_all quando una struct è uniforme e vuoi meno rumore. serde offre anche altre convenzioni come "snake_case", "kebab-case" e "SCREAMING_SNAKE_CASE" per lo stesso attributo.

Parole chiave di Rust e chiavi non identificatore

Le chiavi JSON sono solo stringhe, quindi niente impedisce a un’API di inviare una chiave chiamata type o match, entrambe parole riservate in Rust. Il generatore le sanifica trasformandole in un identificatore valido più 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 forma con underscore finale (type_) è scelta deliberatamente al posto di un identificatore raw come r#type. Gli identificatori raw non possono esprimere ogni parola chiave in ogni posizione; in particolare self, crate e super vengono rifiutati come identificatori raw, quindi un approccio sanifica-più-rename è l’unico che compila sempre. Le chiavi non identificatore come first-name o una 2fa che inizia con una cifra ricevono lo stesso trattamento: un nome Rust valido e un rename che riporta alla chiave JSON letterale.

Deserializzare con serde_json

Una volta ottenuta la struct, aggiungi le dipendenze a Cargo.toml:

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

Poi basta una sola riga per il parsing. Questo esempio è completo e compila così com’è:

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(())
}

Il derive Deserialize fa tutto il lavoro. Usa serde_json::from_str per una stringa, from_slice per uno slice di byte, o from_reader per un file o un corpo HTTP, e serde_json::to_string per serializzare un valore di nuovo in JSON. È questa la ricompensa per aver azzeccato i tipi: il serde derive che hai generato è un validatore a runtime, quindi una chiamata di deserializzazione serde_json che restituisce Ok è la garanzia che i dati corrispondono alla tua struct, non solo la convinzione del compilatore che lo facciano.

Date, chiavi dinamiche e campi sconosciuti

Alcune forme non si mappano su un semplice campo di struct, e il generatore assegna loro un default ragionevole che poi affini a mano.

Date. JSON non ha un tipo data, quindi un timestamp ISO o RFC 3339 arriva come semplice String. Per una vera gestione delle date, cambia il campo in un tipo chrono e abilita la feature serde di chrono (chrono = { version = "0.4", features = ["serde"] }). serde a quel punto analizza 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>,
}

Chiavi dinamiche. Quando le chiavi variano a runtime, come una mappa da ID a valori, una struct fissa è la forma sbagliata. Usa una HashMap con chiave String:

use std::collections::HashMap;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub scores: HashMap<String, i64>,
}

Campi extra sconosciuti. Per mantenere una struct tipizzata ma catturare comunque le chiavi che non hai modellato, aggiungi un campo #[serde(flatten)] basato su una HashMap. Per un valore davvero dinamico, serde_json::Value è la soluzione universale. Quando la vera esigenza è validare la struttura invece di generare i tipi, la guida alla validazione con JSON Schema spiega come imporre un contratto dall’inizio alla fine.

json-to-rust vs quicktype vs typeshare vs manuale

Non esiste un unico modo migliore per produrre tipi Rust da JSON. Dipende da dove risiede il JSON e in quale direzione stai convertendo.

ApproccioIdeale perNota
Convertitore online (questo strumento, transform.tools)Conversioni una tantum, payload sensibili, zero installazioniInferisce da un campione, interamente lato client
quicktypeOutput multilinguaggio, codegen in pipelineAnch’esso guidato da un campione; CLI o web
typeshareCondividere tipi Rust esistenti con TS, Swift, KotlinDirezione opposta: da Rust ad altri linguaggi
schemafy / typifyGenerare da un JSON SchemaL’input è uno schema, non un campione
Scrivere le struct a manoPayload minuscoli, imparare serdeControllo totale, noioso e soggetto a errori su larga scala

La distinzione da interiorizzare: un convertitore online e quicktype inferiscono entrambi i tipi da un campione di JSON, mentre typeshare va nella direzione opposta, trasformando tipi Rust annotati in TypeScript o Swift. schemafy e typify prendono in input un JSON Schema, non dati di esempio. Se il tuo codebase è TypeScript anziché Rust, lo stesso approccio guidato da un campione vale con il convertitore da JSON a TypeScript, e la guida alle interfacce da JSON a TypeScript approfondisce quel versante.

Insidie comuni quando si genera Rust da JSON

Le struct generate sono un punto di partenza. Fai attenzione a questi aspetti prima di fidarti dell’output su dati reali.

  • Un solo campione raramente rivela ogni forma. I campi opzionali si possono inferire solo da più elementi di un array. Incolla un array rappresentativo così l’inferenza degli Option è accurata invece di un’ipotesi basata su un unico oggetto fortunato.
  • Gli array vuoti o misti ripiegano su serde_json::Value. È la risposta onesta quando non c’è nulla da cui inferire. Fornisci un campione più ricco per ottenere un tipo di elemento concreto.
  • Non restringere un ID grande a i32. Gli ID snowflake e simili superano 2^53 e vanno in overflow in un campo a 32 bit. Mantieni l’i64 o l’u64 generato.
  • Un campo sempre null diventa Option<serde_json::Value>, non un semplice opzionale. Lo strumento non ha alcun tipo da inferire, quindi trattalo come un segnaposto e assegnagli un tipo reale non appena ne conosci uno.
  • serde_json::Value richiede la dipendenza serde_json. Se l’output contiene Value e nel tuo Cargo.toml manca il crate, il codice non compilerà.
  • Una data lasciata come String non permette calcoli sulle date. Se prevedi di confrontare o formattare timestamp, passa a un tipo chrono invece di analizzare le stringhe a mano.

Domande frequenti

Perché serde rifiuta il mio JSON anche se la struct sembra corretta?

Quasi sempre un tipo non corrispondente. Le due cause abituali sono un valore in virgola mobile che finisce in un campo i64, e una chiave a volte assente ma non tipizzata come Option. serde impone il tipo in fase di deserializzazione, quindi correggi il campo in f64 o Option<T>, oppure rigenera la struct da un campione rappresentativo.

In quale tipo numerico Rust dovrebbe diventare un intero JSON?

Come predefinito usa i64. Promuovi a u64 quando un valore supera i64::MAX, e ripiega su f64 oltre u64::MAX. Qualsiasi numero scritto con un punto decimale o un esponente si mappa su f64 a prescindere dalla dimensione, perché serde rifiuta un float deserializzato in un campo intero.

Come rendo opzionale un campo quando JSON lo omette?

Tipizzalo come Option<T>. serde tratta automaticamente Option come opzionale e deserializza una chiave mancante come None, quindi non ti serve #[serde(default)]. Il convertitore marca un campo come Option quando ad alcuni elementi del campione manca la chiave.

Dovrei usare #[serde(rename)] o #[serde(rename_all)]?

Usa #[serde(rename)] per singolo campo quando un payload mescola stili di denominazione; funziona sempre. Se ogni campo di una struct segue una sola convenzione, elimina gli attributi per singolo campo e metti invece un unico #[serde(rename_all = "camelCase")] sulla struct. Entrambi deserializzano in modo identico.

Come gestisco le chiavi JSON che sono parole chiave di Rust come type?

Il generatore emette type_ con un #[serde(rename = "type")] che rimappa alla chiave originale. È più robusto di un identificatore raw come r#type, perché gli identificatori raw non possono coprire self, crate e super in ogni posizione.

Perché le date JSON sono tipizzate come String invece che come un tipo data?

JSON non ha un tipo data, quindi un timestamp è solo una stringa in transito, e String è il default onesto. Per una vera gestione delle date, cambia il campo nel DateTime<Utc> o NaiveDate di chrono e abilita la feature serde di chrono; serde a quel punto analizza RFC 3339 al posto tuo.

Come deserializzo JSON nella struct generata?

Aggiungi serde_json a Cargo.toml, poi scrivi let root: Root = serde_json::from_str(json)?;. Il derive Deserialize fa il resto. Usa from_slice per uno slice di byte e from_reader per un file o un corpo HTTP.

Il mio JSON resta privato usando un convertitore online da JSON a struct Rust?

Sì. La conversione avviene al 100% nel tuo browser con JavaScript. Il tuo JSON, inclusi token, ID e dati dei clienti, non lascia mai la pagina e non viene mai inviato a un server. Quando sei pronto, prova il convertitore da JSON a struct Rust sul tuo payload.

Tag: rust json serde type-safety developer-tools

Articoli correlati

Vedi tutti gli articoli