Step 2 of 6
From JSON back into structs
The other direction is deserialization: you derive Deserialize and ask serde_json::from_str for a value of your type.
use serde::Deserialize;
#[derive(Deserialize)]
struct Reading {
sensor: String,
celsius: f64,
}
fn main() {
let text = r#"{"sensor": "greenhouse", "celsius": 21.5, "battery": 87}"#;
let r: Reading = serde_json::from_str(text).unwrap();
println!("{} reads {:.1} C", r.sensor, r.celsius);
}
greenhouse reads 21.5 C
Piece by piece:
r#"..."#is a raw string literal: backslashes and double quotes inside it are taken literally, so JSON can be pasted in without escaping every". (The number of#can grow if the text itself contains"#.)from_stris generic: it can produce any type that implementsDeserialize, so you must say which one. Here the annotationlet r: Readingdoes it; the turbofishserde_json::from_str::<Reading>(text)works too.- It returns
Result<Reading, serde_json::Error>, because input from outside can be anything. - The JSON has a
batteryfield thatReadingdoesn't. Unknown fields are ignored by default, which makes your program robust when an API adds fields later. - Use owned types like
Stringin structs you deserialize. (Borrowing&strfields from the input is possible but needs lifetimes and fails on strings with escapes; reach for it only when profiling says so.)
What can go wrong
serde checks the shape of the data against your type: every field present, every value the right type and in range. Anything else is an Err whose message says what and where:
use serde::Deserialize;
#[derive(Deserialize)]
struct Reading {
sensor: String,
celsius: f64,
}
fn main() {
let inputs = [
r#"{"sensor": "attic", "celsius": 30}"#,
r#"{"sensor": "attic"}"#,
r#"{"sensor": "attic", "celsius": "hot"}"#,
r#"{"sensor": "attic", "celsius": 30"#,
];
for text in inputs {
match serde_json::from_str::<Reading>(text) {
Ok(r) => println!("ok: {} = {}", r.sensor, r.celsius),
Err(e) => println!("error: {e}"),
}
}
}
ok: attic = 30
error: missing field `celsius` at line 1 column 19
error: invalid type: string "hot", expected f64 at line 1 column 36
error: EOF while parsing an object at line 1 column 33
A JSON integer like 30 is fine for an f64 field. The reverse isn't: 2.5 for a u32 is an error, and so is -3 for a u32 or 300 for a u8. Your Rust types are the validation: pick them carefully.
Common mistakes
- No type for
from_str.let r = serde_json::from_str(text).unwrap();with nothing telling Rust the target type fails witherror[E0282]: type annotations needed. - Unwrapping input you don't control.
from_str(...).unwrap()on data from a user, a file or the network turns a typo into a crash. Return the error with?or handle it withmatch. - Deriving
Deserializebut notSerializeand then callingto_string: they're separate traits. Derive both when you need both directions:#[derive(Serialize, Deserialize)].
Your task
A shop sends carts as JSON like {"items": [{"name": "mug", "price": 2.5, "qty": 2}]}. Derive what's needed and write fn cart_total(json: &str) -> Result<f64, serde_json::Error> that parses the cart and returns the sum of price * qty over all items. Invalid JSON must give an Err (use ?).
Challenge 2
Read a JSON array of players from standard input, like [{"name": "Ferris", "score": 950}, ...], and print the best one as top: Ferris (950). If the array is empty, print no players. You can assume the input is valid JSON.
Challenge 3
A round trip: write fn rename_sensor(json: &str, new_name: &str) -> Result<String, serde_json::Error> that parses a sensor reading, changes its sensor field to new_name, and returns it serialized again as compact JSON. The struct needs both derives. Errors go back to the caller with ?.
Previous: From structs to JSON Next: Shaping the JSON: rename, default, skip