How to deserialize JSON into a struct
Short answer: Derive serde::Deserialize on a struct whose field names match the JSON’s keys, then hand the text to serde_json::from_str. When a key differs — fullName in the JSON, full_name in Rust — rename the field with #[serde(rename = "fullName")]. serde matches names literally and never guesses.
From a fresh project — cargo new json-demo — Cargo.toml needs two crates:
[dependencies]serde = { version = "1.0.229", features = ["derive"] }serde_json = "1.0.154"The exact versions depend on when you run cargo add; the derive feature is the part that matters. Without it the macro does not exist, and #[derive(Deserialize)] stops the build with “cannot find derive macro Deserialize in this scope”.
The struct, and from_str
Section titled “The struct, and from_str”use serde::Deserialize;
#[derive(Debug, Deserialize)]struct User { id: u64, name: String, active: bool,}
fn main() { let text = r#"{"id": 7, "name": "Grace", "active": true}"#; let user: User = serde_json::from_str(text).unwrap(); println!("{user:?}");}User { id: 7, name: "Grace", active: true }The derive writes the code that walks the JSON; from_str returns a Result<User, serde_json::Error>. unwrap is fine here because the payload is a literal on the line above — JSON arriving from somewhere else deserves a match, not a panic. Each field takes the value under the key of the same name: u64 from a number, String from a string, bool from true. The names have to match exactly, and nothing in the compiler checks that before the code runs.
When the key doesn’t match the field
Section titled “When the key doesn’t match the field”An API that spells the key fullName while the struct says full_name is not a compile error. Both sides look right, and the failure waits for the first response:
use serde::Deserialize;
#[derive(Debug, Deserialize)]struct User { id: u64, full_name: String,}
fn main() { let text = r#"{"id": 7, "fullName": "Grace Hopper"}"#; match serde_json::from_str::<User>(text) { Ok(user) => println!("{user:?}"), Err(e) => println!("{e}"), }}missing field `full_name` at line 1 column 37serde matched id, then reached the end of the object still waiting for full_name — the error names the Rust field, because from serde’s side that is the one that is missing. The line and column sit one past the closing brace: that is where the object ended, not where the mismatched key sits. Naming the field in the error is what makes the fix obvious.
#[serde(rename = …)]
Section titled “#[serde(rename = …)]”use serde::Deserialize;
#[derive(Debug, Deserialize)]struct User { id: u64, #[serde(rename = "fullName")] full_name: String,}
fn main() { let text = r#"{"id": 7, "fullName": "Grace Hopper"}"#; let user: User = serde_json::from_str(text).unwrap(); println!("{user:?}");}User { id: 7, full_name: "Grace Hopper" }rename takes the key exactly as the API spells it, and the Rust side keeps snake_case. The attribute works in both directions — the same struct serialised back produces "fullName" — and turning a struct into JSON is what the next article in this chapter covers. When every key in one payload follows the same convention, rename_all renames them all in one attribute; that gets its own page later here.
- How to let a CLI read a config file — the same derive, reading TOML instead of JSON
- Reading defaults from a config file — serde inside a real CLI
- Back to Working with JSON, TOML and CSV