JSON con Serde in Rust
Il JSON è ovunque: file di configurazione, API web, salvataggi dei giochi. In Rust quasi tutti usano due crate per lavorarci. serde (abbreviazione di serialize / deserialize, serializzare e deserializzare) sa trasformare i valori Rust in dati e viceversa. serde_json gli insegna il formato JSON.
Pensa a serde come a un traduttore. Descrivi i tuoi dati una volta sola, come una normale struct Rust, e serde li traduce in JSON e ritorno al posto tuo. Non scrivi mai il testo JSON a mano, e non devi mai smontare il JSON carattere per carattere.
Configurazione
Sezione intitolata “Configurazione”Dentro il tuo progetto, aggiungi entrambi i crate. La feature derive permette a serde di scrivere per te il codice di traduzione:
cargo add serde --features derivecargo add serde_jsonOra il tuo Cargo.toml contiene:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Se Cargo.toml e cargo add ti sono nuovi, leggi prima Cargo e crate. Il Rust Playground ha già entrambi i crate, quindi ogni pulsante «Provalo tu» di questa pagina funziona senza configurare niente.
Da struct a JSON
Sezione intitolata “Da struct a JSON”Aggiungi #[derive(Serialize)] a una struct, e serde_json::to_string può trasformarla in testo 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);}Output:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string ti dà un JSON compatto, adatto da mandare in rete. to_string_pretty aggiunge a capo e spazi, più comodo per i file che leggeranno delle persone. Le chiavi mantengono lo stesso ordine dei campi della struct.
Da JSON a struct
Sezione intitolata “Da JSON a struct”La direzione opposta si chiama deserializzare. Aggiungi Deserialize all’elenco del derive, poi chiama serde_json::from_str. Gli dici quale tipo vuoi con un’annotazione di 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!("{} ha {} anni", user.name, user.age); Ok(())}Output:
User { name: "Ana", age: 28, admin: false }Ana ha 28 anniDue cose da notare:
r#"..."#è una stringa raw (grezza). Dentro puoi scrivere"liberamente, cosa comodissima per il JSON.from_strrestituisce unResult, perché il testo potrebbe non essere JSON valido. QuimainrestituisceResult, quindi possiamo usare?per passare in alto qualsiasi errore.
Quando il JSON è sbagliato
Sezione intitolata “Quando il JSON è sbagliato”Il JSON vero spesso non è come te lo aspetti. Manca un campo, oppure un numero arriva come testo. serde controlla tutto e ti dà un errore chiaro invece di far crollare il programma. Usa match per gestirlo:
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!("Errore: {}", e), } }}Output:
OK: User { name: "Ana", age: 28 }Errore: missing field `age` at line 1 column 15Errore: invalid type: string "old", expected u32 at line 1 column 27I messaggi di serde sono in inglese: il primo dice che manca il campo age (riga 1, colonna 15), il secondo che è arrivata la stringa "old" dove serviva un u32. Il JSON di prova resta in inglese apposta, perché i numeri di colonna dipendono dalla lunghezza del testo. La parte ::<User> si chiama «turbofish». È un altro modo per dire quale tipo vuoi, utile quando non c’è un let user: User su cui mettere l’annotazione.
Chiavi mancanti: Option e #[serde(default)]
Sezione intitolata “Chiavi mancanti: Option e #[serde(default)]”E se un campo è davvero facoltativo? Hai due buone scelte:
- Rendi il campo un
Option. Una chiave mancante (onull) diventaNone. - Aggiungi
#[serde(default)]. Una chiave mancante riceve il valore predefinito del tipo:0,false, unaStringvuota o unVecvuoto.
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": "Ama 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);}Output:
Profile { name: "Ana", bio: Some("Ama Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }Usa Option quando «nessun valore» vuol dire qualcosa di diverso da «zero» o «vuoto». Usa default quando il valore predefinito va abbastanza bene.
Rinominare i campi
Sezione intitolata “Rinominare i campi”Rust usa lo snake_case per i nomi dei campi, ma molte API web usano il camelCase, e alcune usano nomi che non sono proprio Rust valido (come type). Non devi rinunciare a dei bei nomi Rust. Usa rename_all per tutta la struct, e rename per un singolo campo:
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() { // il tipo di risposta che potrebbe mandare un'API web let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{} (id {}) ha un account {}", profile.display_name, profile.user_id, profile.account_type);
// la serializzazione usa gli stessi nomi, quindi il JSON fa andata e ritorno println!("{}", serde_json::to_string(&profile).unwrap());}Output:
Ana Silva (id 42) ha un account premium{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}Di default serde semplicemente ignora le chiavi JSON che la tua struct non ha. Quindi ti basta descrivere i campi che ti interessano, anche se l’API ne manda cinquanta.
Liste di cose: Vec di struct
Sezione intitolata “Liste di cose: Vec di struct”Un array JSON diventa un Vec. Le struct possono anche contenere altre struct e altri vettori, quindi puoi descrivere un JSON profondo semplicemente annidando i tipi:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "Imparare serde", "done": true}, {"title": "Scrivere un file di configurazione", "done": false}, {"title": "Chiamare un'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!("{} attività ancora da fare", left);}Output:
[x] Imparare serde[ ] Scrivere un file di configurazione[ ] Chiamare un'API2 attività ancora da fareJSON senza tipi: Value e json!
Sezione intitolata “JSON senza tipi: Value e json!”A volte non conosci in anticipo la forma del JSON, oppure te ne serve solo un pezzetto. In questi casi usa serde_json::Value. Può contenere qualsiasi JSON: un oggetto, un array, una stringa, un numero, un booleano o null. La macro json! costruisce un Value con una sintassi simile al JSON:
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "Lisbona", "temps": [18, 21, 19], "sunny": true });
println!("Città: {}", data["city"]); println!("Prima temperatura: {}", data["temps"][0]);
// as_str() ti dà un semplice &str invece di una stringa JSON if let Some(city) = data["city"].as_str() { println!("Testo semplice: {}", city); }
// una chiave mancante dà Null invece di far crollare il programma println!("Vento: {}", data["wind"]);}Output:
Città: "Lisbona"Prima temperatura: 18Testo semplice: LisbonaVento: nullNota che stampare data["city"] mostra le virgolette: è ancora una stringa JSON. Usa .as_str(), .as_i64() o .as_bool() per ottenere valori Rust semplici. Ognuno restituisce un Option, perché il valore potrebbe essere di un altro tipo.
Value è flessibile, ma perdi l’aiuto del compilatore. Preferisci una struct ogni volta che conosci la forma.
Leggere e scrivere un file JSON
Sezione intitolata “Leggere e scrivere un file JSON”Ecco un esempio dal mondo reale: salvare le impostazioni di un’app in config.json e caricarle di nuovo. Combina questa pagina con quella sui file. std::fs::write salva il testo e std::fs::read_to_string lo ricarica:
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("scuro"), font_size: 14, };
// salva let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// carica let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("Caricato: {:?}", loaded); Ok(())}Output:
Caricato: Config { username: "ana", theme: "scuro", font_size: 14 }Perché Box<dyn Error>? Questo main può fallire in due modi diversi: un errore sui file (std::io::Error) o un errore JSON (serde_json::Error). Box<dyn Error> vuol dire «qualsiasi tipo di errore», quindi ? funziona con entrambi. La pagina sugli errori personalizzati lo spiega più nel dettaglio.
Errori comuni
Sezione intitolata “Errori comuni”Dimenticare di derivare Deserialize. Se la tua struct deriva solo Serialize, puoi scrivere JSON ma non leggerlo. L’errore è lungo, ma le righe importanti sono queste:
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` typeIl compilatore dice (in inglese) che User non implementa Deserialize e suggerisce di aggiungere #[derive(serde::Deserialize)]. La correzione: #[derive(Serialize, Deserialize)]. Se invece vedi «cannot find derive macro Serialize», hai aggiunto serde senza la feature derive. Esegui cargo add serde --features derive.
Usare .unwrap() su dati che non controlli. Va bene nei piccoli esempi, ma il JSON che arriva da un utente o da un sito può essere rotto. Un file sbagliato farà crollare il programma con un panic. Usa ? o match, come visto sopra.
Aspettarsi che data["name"] venga stampato senza virgolette. Un Value si stampa come JSON. Chiama .as_str() per avere testo semplice.
Pratica
Sezione intitolata “Pratica”1. Salva una lista della spesa
Sezione intitolata “1. Salva una lista della spesa”Crea una struct Item con un name (String) e una qty (u32). Metti due articoli in un Vec, trasforma tutto il vettore in JSON con una sola chiamata e stampalo.
Suggerimento: solo la struct ha bisogno di #[derive(Serialize)]. Anche un Vec di cose serializzabili è serializzabile, e diventa un array JSON.
Mostra la soluzione
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("mele"), qty: 3 }, Item { name: String::from("pane"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}Output:
[{"name":"mele","qty":3},{"name":"pane","qty":1}]serde sa già come gestire Vec, String e i numeri, quindi basta derivare Serialize su Item. Prova to_string_pretty per vedere la stessa lista distribuita su più righe.
2. Impostazioni con valori predefiniti
Sezione intitolata “2. Impostazioni con valori predefiniti”Leggi questo JSON in una struct Settings: {"siteTitle": "Il mio blog", "postsPerPage": 5}. La struct deve avere site_title, posts_per_page e un theme che può mancare. Poi leggi anche {"siteTitle": "Appunti"}, dove posts_per_page deve tornare a 0. Stampali entrambi con {:?}.
Suggerimento: ti servono #[serde(rename_all = "camelCase")] sulla struct, Option<String> per theme e #[serde(default)] su posts_per_page.
Mostra la soluzione
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": "Il mio blog", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "Appunti"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Output:
Settings { site_title: "Il mio blog", posts_per_page: 5, theme: None }Settings { site_title: "Appunti", posts_per_page: 0, theme: None }rename_all collega siteTitle a site_title e postsPerPage a posts_per_page. Nessuno dei due JSON ha un theme, quindi è None entrambe le volte, e il postsPerPage mancante nel secondo diventa 0. Togli #[serde(default)] e il secondo unwrap() va in panic con «missing field postsPerPage» (campo mancante). Qui .unwrap() va bene perché il JSON l’abbiamo scritto noi; per un input vero, usa ? o match.
Riepilogo
Sezione intitolata “Riepilogo”- Aggiungi
serde(con--features derive) eserde_json, poi metti#[derive(Serialize, Deserialize)]sulle tue struct. serde_json::to_string/to_string_prettytrasformano un valore in JSON;serde_json::from_strtrasforma il JSON in un valore.from_strrestituisce unResult. Con un input vero gestiscilo con?omatch, non con.unwrap().- Usa
Optiono#[serde(default)]per le chiavi che possono mancare, erename/rename_allper far corrispondere i nomi del JSON. - Usa
serde_json::Valueejson!quando non conosci la forma. Quando la conosci, preferisci le struct.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quale derive ti permette di trasformare un testo JSON nella tua struct?
2. Un oggetto JSON potrebbe non contenere la chiave bio. Qual è un buon tipo per quel campo?
3. La tua struct ha un campo display_name, ma il JSON usa displayName. Qual è la correzione più semplice per tutta la struct?
4. Cosa fa serde, di default, con le chiavi JSON che la tua struct non ha?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- serde.rs — la guida ufficiale di Serde (in inglese): una panoramica di come funziona serde, con esempi per tanti formati di dati.
- Container attributes — serde.rs (in inglese): le opzioni per un’intera struct o enum, come
rename_alledeny_unknown_fields. - Field attributes — serde.rs (in inglese): le opzioni per i singoli campi, come
rename,default,skipeskip_serializing_if. serde_json— docs.rs (in inglese): la pagina principale del crate, con esempi di JSON con tipi e senza tipi e della macrojson!.serde_json::Value— docs.rs (in inglese): tutti i metodi per leggere JSON senza tipi, comeas_str,as_i64eget.