JSON mit Serde in Rust
JSON begegnet dir überall: in Konfigurationsdateien, bei Web-APIs, in gespeicherten Spielständen. In Rust benutzen fast alle dafür zwei Crates. serde (kurz für serialisieren / deserialisieren) weiß, wie man Rust-Werte in Daten umwandelt und wieder zurück. serde_json bringt ihm das Format JSON bei.
Stell dir serde als Übersetzer vor. Du beschreibst deine Daten einmal als ganz normales Rust-Struct, und serde übersetzt sie für dich nach JSON und zurück. Du schreibst den JSON-Text nie von Hand und musst JSON auch nie Zeichen für Zeichen auseinandernehmen.
Einrichtung
Abschnitt betitelt „Einrichtung“Füge in deinem Projekt beide Crates hinzu. Mit dem Feature derive schreibt serde den Übersetzungscode für dich:
cargo add serde --features derivecargo add serde_jsonIn deiner Cargo.toml steht jetzt:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Wenn Cargo.toml und cargo add neu für dich sind, lies zuerst Cargo und Crates. Der Rust Playground hat beide Crates schon dabei, deshalb funktioniert jeder „Selbst ausprobieren“-Button auf dieser Seite ohne Einrichtung.
Vom Struct zu JSON
Abschnitt betitelt „Vom Struct zu JSON“Schreib #[derive(Serialize)] über ein Struct, und schon kann serde_json::to_string es in JSON-Text umwandeln:
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);}Ausgabe:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string liefert kompaktes JSON – gut, um es über das Netzwerk zu schicken. to_string_pretty fügt Zeilenumbrüche und Leerzeichen ein. Das ist angenehmer für Dateien, die Menschen lesen sollen. Die Keys erscheinen in derselben Reihenfolge wie die Felder in deinem Struct.
Von JSON zum Struct
Abschnitt betitelt „Von JSON zum Struct“Den umgekehrten Weg nennt man Deserialisieren. Füge Deserialize zur derive-Liste hinzu und rufe serde_json::from_str auf. Welchen Typ du haben willst, sagst du mit einer Typangabe:
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!("{} ist {} Jahre alt", user.name, user.age); Ok(())}Ausgabe:
User { name: "Ana", age: 28, admin: false }Ana ist 28 Jahre altZwei Dinge fallen auf:
r#"..."#ist ein Raw String (roher String). Darin kannst du"ganz frei schreiben, was bei JSON sehr praktisch ist.from_strgibt einResultzurück, weil der Text vielleicht kein gültiges JSON ist. Hier gibtmaineinResultzurück, also können wir mit?jeden Fehler nach oben weiterreichen.
Wenn das JSON nicht stimmt
Abschnitt betitelt „Wenn das JSON nicht stimmt“Echtes JSON sieht oft anders aus als erwartet. Ein Feld fehlt, oder eine Zahl kommt als Text an. serde prüft alles und gibt dir eine klare Fehlermeldung, statt abzustürzen. Mit match behandelst du sie:
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": "alt"}"#, ];
for text in inputs { match serde_json::from_str::<User>(text) { Ok(user) => println!("OK: {:?}", user), Err(e) => println!("Fehler: {}", e), } }}Ausgabe:
OK: User { name: "Ana", age: 28 }Fehler: missing field `age` at line 1 column 15Fehler: invalid type: string "alt", expected u32 at line 1 column 27Die Fehlermeldungen selbst kommen von serde_json und sind deshalb auf Englisch. Die erste bedeutet „Feld age fehlt in Zeile 1, Spalte 15“, die zweite „ungültiger Typ: String “alt”, erwartet wurde u32 in Zeile 1, Spalte 27“.
Der Teil ::<User> heißt „Turbofish“. Auch damit gibst du an, welchen Typ du haben willst. Das ist praktisch, wenn es kein let user: User gibt, an das du die Typangabe schreiben könntest.
Fehlende Keys: Option und #[serde(default)]
Abschnitt betitelt „Fehlende Keys: Option und #[serde(default)]“Was, wenn ein Feld wirklich optional ist? Du hast zwei gute Möglichkeiten:
- Mach das Feld zu einer
Option. Ein fehlender Key (odernull) wird zuNone. - Schreib
#[serde(default)]davor. Ein fehlender Key bekommt dann den Standardwert des Typs:0,false, einen leerenStringoder einen leerenVec.
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": "Liebt 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);}Ausgabe:
Profile { name: "Ana", bio: Some("Liebt Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }Nimm Option, wenn „kein Wert“ etwas anderes bedeutet als „null“ oder „leer“. Nimm default, wenn der Standardwert gut genug ist.
Felder umbenennen
Abschnitt betitelt „Felder umbenennen“Rust benutzt für Feldnamen snake_case, viele Web-APIs dagegen camelCase. Manche benutzen sogar Namen, die in Rust gar nicht erlaubt sind (wie type). Auf schöne Rust-Namen musst du trotzdem nicht verzichten. Benutze rename_all für das ganze Struct und rename für ein einzelnes Feld:
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() { // so etwa könnte die Antwort einer Web-API aussehen let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{} (ID {}) hat ein Konto vom Typ {}", profile.display_name, profile.user_id, profile.account_type);
// beim Serialisieren werden dieselben Namen benutzt, das JSON kommt also unverändert zurück println!("{}", serde_json::to_string(&profile).unwrap());}Ausgabe:
Ana Silva (ID 42) hat ein Konto vom Typ premium{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}JSON-Keys, die es in deinem Struct nicht gibt, ignoriert serde standardmäßig einfach. Du musst also nur die Felder beschreiben, die dich interessieren – selbst wenn die API fünfzig davon schickt.
Listen: ein Vec von Structs
Abschnitt betitelt „Listen: ein Vec von Structs“Aus einem JSON-Array wird ein Vec. Structs können auch andere Structs und Vektoren enthalten. So beschreibst du selbst tief verschachteltes JSON, indem du einfach Typen ineinandersteckst:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "serde lernen", "done": true}, {"title": "Eine Konfigurationsdatei schreiben", "done": false}, {"title": "Eine API aufrufen", "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!("Noch {} Aufgaben offen", left);}Ausgabe:
[x] serde lernen[ ] Eine Konfigurationsdatei schreiben[ ] Eine API aufrufenNoch 2 Aufgaben offenJSON ohne festen Typ: Value und json!
Abschnitt betitelt „JSON ohne festen Typ: Value und json!“Manchmal kennst du die Form des JSON nicht im Voraus, oder du brauchst nur ein kleines Stück davon. Dafür gibt es serde_json::Value. Ein Value kann beliebiges JSON enthalten: ein Objekt, ein Array, einen String, eine Zahl, einen Bool oder null. Mit dem Makro json! baust du einen Value in JSON-ähnlicher Schreibweise:
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "Lissabon", "temps": [18, 21, 19], "sunny": true });
println!("Stadt: {}", data["city"]); println!("Erste Temperatur: {}", data["temps"][0]);
// as_str() liefert dir einen einfachen &str statt eines JSON-Strings if let Some(city) = data["city"].as_str() { println!("Reiner Text: {}", city); }
// ein fehlender Key liefert Null, statt abzustürzen println!("Wind: {}", data["wind"]);}Ausgabe:
Stadt: "Lissabon"Erste Temperatur: 18Reiner Text: LissabonWind: nullBeim Ausgeben von data["city"] siehst du Anführungszeichen, denn es ist immer noch ein JSON-String. Mit .as_str(), .as_i64() oder .as_bool() bekommst du einfache Rust-Werte. Jede dieser Methoden gibt eine Option zurück, weil der Wert auch einen anderen Typ haben könnte.
Value ist flexibel, aber dabei verlierst du die Hilfe des Compilers. Nimm lieber ein Struct, wann immer du die Form kennst.
Eine JSON-Datei lesen und schreiben
Abschnitt betitelt „Eine JSON-Datei lesen und schreiben“Hier ein Beispiel aus der Praxis: App-Einstellungen in config.json speichern und wieder laden. Es verbindet diese Seite mit Dateien in Rust lesen und schreiben. std::fs::write speichert den Text, und std::fs::read_to_string lädt ihn wieder:
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("dark"), font_size: 14, };
// speichern let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// laden let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("Geladen: {:?}", loaded); Ok(())}Ausgabe:
Geladen: Config { username: "ana", theme: "dark", font_size: 14 }Warum Box<dyn Error>? Dieses main kann auf zwei verschiedene Arten fehlschlagen: mit einem Dateifehler (std::io::Error) oder mit einem JSON-Fehler (serde_json::Error). Box<dyn Error> bedeutet „irgendeine Art von Fehler“, deshalb funktioniert ? für beide. Genauer erklärt das die Seite Eigene Fehlertypen.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“Deserialize vergessen. Wenn dein Struct nur Serialize ableitet, kannst du JSON schreiben, aber nicht lesen. Die Fehlermeldung ist lang, aber das sind die wichtigen Zeilen:
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` typeDer Compiler sagt: User implementiert den Trait Deserialize nicht, und er schlägt vor, #[derive(serde::Deserialize)] hinzuzufügen. Die Lösung lautet also #[derive(Serialize, Deserialize)]. Siehst du stattdessen „cannot find derive macro Serialize“ (das Derive-Makro Serialize wurde nicht gefunden), hast du serde ohne das Feature derive hinzugefügt. Führe cargo add serde --features derive aus.
.unwrap() bei Daten, die du nicht kontrollierst. In kleinen Beispielen ist das in Ordnung. Aber JSON von Benutzern oder von einer Website kann kaputt sein, und eine fehlerhafte Datei lässt dein Programm dann mit einem Panic abstürzen. Benutze ? oder match wie oben gezeigt.
Erwarten, dass data["name"] ohne Anführungszeichen ausgegeben wird. Ein Value wird als JSON ausgegeben. Rufe .as_str() auf, um einfachen Text zu bekommen.
Übungen
Abschnitt betitelt „Übungen“1. Eine Einkaufsliste speichern
Abschnitt betitelt „1. Eine Einkaufsliste speichern“Schreib ein Struct Item mit einem name (String) und einer Menge qty (u32). Leg zwei Einträge in einen Vec, wandle den ganzen Vektor mit einem einzigen Aufruf in JSON um und gib es aus.
Tipp: Nur das Struct braucht #[derive(Serialize)]. Ein Vec aus serialisierbaren Werten ist selbst auch serialisierbar und wird zu einem JSON-Array.
Lösung anzeigen
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("Äpfel"), qty: 3 }, Item { name: String::from("Brot"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}Ausgabe:
[{"name":"Äpfel","qty":3},{"name":"Brot","qty":1}]serde weiß schon, wie es mit Vec, String und Zahlen umgeht. Deshalb reicht es, Serialize für Item abzuleiten. Probier auch to_string_pretty aus, um dieselbe Liste über mehrere Zeilen verteilt zu sehen.
2. Einstellungen mit Standardwerten
Abschnitt betitelt „2. Einstellungen mit Standardwerten“Lies dieses JSON in ein Struct Settings ein: {"siteTitle": "Mein Blog", "postsPerPage": 5}. Das Struct soll site_title, posts_per_page und ein theme haben, das fehlen darf. Lies danach auch {"siteTitle": "Notizen"} ein. Dort soll posts_per_page auf 0 zurückfallen. Gib beide mit {:?} aus.
Tipp: Du brauchst #[serde(rename_all = "camelCase")] über dem Struct, Option<String> für theme und #[serde(default)] für posts_per_page.
Lösung anzeigen
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": "Mein Blog", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "Notizen"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Ausgabe:
Settings { site_title: "Mein Blog", posts_per_page: 5, theme: None }Settings { site_title: "Notizen", posts_per_page: 0, theme: None }rename_all ordnet siteTitle dem Feld site_title zu und postsPerPage dem Feld posts_per_page. Keines der beiden JSONs enthält ein theme, also ist es beide Male None. Und das fehlende postsPerPage im zweiten wird zu 0. Entfernst du #[serde(default)], löst das zweite unwrap() einen Panic mit „missing field postsPerPage“ aus (das Feld fehlt). Das .unwrap() ist hier in Ordnung, weil wir das JSON selbst geschrieben haben. Bei echten Eingaben nimmst du ? oder match.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Füge
serde(mit--features derive) undserde_jsonhinzu und schreib#[derive(Serialize, Deserialize)]über deine Structs. serde_json::to_string/to_string_prettymachen aus einem Wert JSON.serde_json::from_strmacht aus JSON einen Wert.from_strgibt einResultzurück. Behandle es bei echten Eingaben mit?odermatch, nicht mit.unwrap().- Nimm
Optionoder#[serde(default)]für Keys, die fehlen können, undrename/rename_all, um die Namen an das JSON anzupassen. - Benutze
serde_json::Valueundjson!, wenn du die Form nicht kennst. Wenn du sie kennst, nimm lieber Structs.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Mit welchem derive kannst du JSON-Text in dein Struct umwandeln?
2. In einem JSON-Objekt fehlt vielleicht der Key bio. Welcher Typ passt gut für dieses Feld?
3. Dein Struct hat ein Feld display_name, aber das JSON benutzt displayName. Was ist die einfachste Lösung für das ganze Struct?
4. Was macht serde standardmäßig mit JSON-Keys, die es in deinem Struct nicht gibt?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- serde.rs – der offizielle Serde-Leitfaden (auf Englisch): ein Überblick darüber, wie serde funktioniert, mit Beispielen für viele Datenformate.
- Container attributes – serde.rs (auf Englisch): Optionen für ein ganzes Struct oder Enum, etwa
rename_allunddeny_unknown_fields. - Field attributes – serde.rs (auf Englisch): Optionen für einzelne Felder, etwa
rename,default,skipundskip_serializing_if. serde_json– docs.rs (auf Englisch): die Startseite des Crates, mit Beispielen für typisiertes und untypisiertes JSON und das Makrojson!.serde_json::Value– docs.rs (auf Englisch): alle Methoden zum Lesen von untypisiertem JSON, etwaas_str,as_i64undget.