Skip to content
Select theme

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:

Cargo.toml
[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”.

src/main.rs
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.

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:

src/main.rs
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 37

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

src/main.rs
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.