Zum Inhalt springen

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.

Füge in deinem Projekt beide Crates hinzu. Mit dem Feature derive schreibt serde den Übersetzungscode für dich:

Terminal window
cargo add serde --features derive
cargo add serde_json

In 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.

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
}
▶ Selbst ausprobieren

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.

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 alt
▶ Selbst ausprobieren

Zwei Dinge fallen auf:

  • r#"..."# ist ein Raw String (roher String). Darin kannst du " ganz frei schreiben, was bei JSON sehr praktisch ist.
  • from_str gibt ein Result zurück, weil der Text vielleicht kein gültiges JSON ist. Hier gibt main ein Result zurück, also können wir mit ? jeden Fehler nach oben weiterreichen.

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 15
Fehler: invalid type: string "alt", expected u32 at line 1 column 27
▶ Selbst ausprobieren

Die 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.

Was, wenn ein Feld wirklich optional ist? Du hast zwei gute Möglichkeiten:

  • Mach das Feld zu einer Option. Ein fehlender Key (oder null) wird zu None.
  • Schreib #[serde(default)] davor. Ein fehlender Key bekommt dann den Standardwert des Typs: 0, false, einen leeren String oder einen leeren Vec.
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: [] }
▶ Selbst ausprobieren

Nimm Option, wenn „kein Wert“ etwas anderes bedeutet als „null“ oder „leer“. Nimm default, wenn der Standardwert gut genug ist.

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"}
▶ Selbst ausprobieren

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.

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 aufrufen
Noch 2 Aufgaben offen
▶ Selbst ausprobieren

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: 18
Reiner Text: Lissabon
Wind: null
▶ Selbst ausprobieren

Beim 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.

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 }
▶ Selbst ausprobieren

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.

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` type

Der 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.

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}]
▶ Selbst ausprobieren

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.

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 }
▶ Selbst ausprobieren

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.

  • Füge serde (mit --features derive) und serde_json hinzu und schreib #[derive(Serialize, Deserialize)] über deine Structs.
  • serde_json::to_string / to_string_pretty machen aus einem Wert JSON. serde_json::from_str macht aus JSON einen Wert.
  • from_str gibt ein Result zurück. Behandle es bei echten Eingaben mit ? oder match, nicht mit .unwrap().
  • Nimm Option oder #[serde(default)] für Keys, die fehlen können, und rename / rename_all, um die Namen an das JSON anzupassen.
  • Benutze serde_json::Value und json!, wenn du die Form nicht kennst. Wenn du sie kennst, nimm lieber Structs.

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