Working with JSON using serde
Prerequisites: File I/O
The standard library has no JSON support. The ecosystem's standard answer is serde ("serialize/deserialize"), a framework for converting Rust values to and from many data formats, plus a format crate — serde_json for JSON.
You derive two traits on your types:
Serialize— the type can be turned into JSON (or TOML, YAML, …).Deserialize— the type can be built from JSON.
The field names and types of your struct describe the expected JSON shape, and
parsing checks the input against it: a missing field or a wrong type becomes
an Err, not a crash later on.
Setup
cargo add serde --features derive
cargo add serde_json
Cargo.toml then contains entries like these (cargo add writes the full
number of the latest release, such as 1.0.228; any 1.x release works):
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Syntax
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] // first_name <-> "firstName"
struct User {
id: u32,
first_name: String,
#[serde(rename = "e-mail")] // one field with a custom JSON name
email: String,
#[serde(default)] // use Default (false) when the key is missing
admin: bool,
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>, // missing or null -> None
}
fn main() -> Result<(), serde_json::Error> {
let json = r#"{"id": 1, "firstName": "Ada", "e-mail": "ada@example.com"}"#;
let user: User = serde_json::from_str(json)?; // JSON text -> struct
let back: String = serde_json::to_string(&user)?; // struct -> compact JSON
println!("{user:?}");
println!("{back}");
Ok(())
}
This prints:
User { id: 1, first_name: "Ada", email: "ada@example.com", admin: false, nickname: None }
{"id":1,"firstName":"Ada","e-mail":"ada@example.com","admin":false}
r#"..."# is a raw string literal: quotes inside it need no escaping,
which is convenient for JSON. A field of type Option<T> may be missing from
the input; skip_serializing_if leaves it out of the output when it is None
instead of writing "nickname":null.
Example
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Book {
title: String,
page_count: u32,
#[serde(rename = "isbn13")]
isbn: String,
#[serde(skip_serializing_if = "Option::is_none")]
subtitle: Option<String>,
tags: Vec<String>,
}
fn main() -> Result<(), serde_json::Error> {
// Parse JSON into a typed struct.
let input = r#"
{
"title": "The Rust Programming Language",
"pageCount": 560,
"isbn13": "978-1718503106",
"tags": ["rust", "beginner"]
}
"#;
let book: Book = serde_json::from_str(input)?;
println!("{} ({} pages)", book.title, book.page_count);
println!("isbn {}, subtitle {:?}, tags {:?}", book.isbn, book.subtitle, book.tags);
// Turn a struct into pretty-printed JSON.
let draft = Book {
title: String::from("Rust Notes"),
page_count: 42,
isbn: String::from("000-0000000000"),
subtitle: Some(String::from("Level 2")),
tags: vec![],
};
println!("{}", serde_json::to_string_pretty(&draft)?);
// Invalid input becomes an Err with a helpful message.
let broken = r#"{"title": "Oops", "pageCount": "many"}"#;
match serde_json::from_str::<Book>(broken) {
Ok(_) => println!("parsed?"),
Err(e) => println!("error: {e}"),
}
// Untyped JSON: serde_json::Value and the json! macro.
let value: Value = json!({ "name": "ferris", "langs": ["rust", "c"], "age": 9 });
let langs = value["langs"].as_array().map(|a| a.len()).unwrap_or(0);
println!("{} knows {} languages", value["name"], langs);
println!("name as &str: {:?}", value["name"].as_str());
println!("missing key gives: {}", value["nope"]);
Ok(())
}
Expected output
The Rust Programming Language (560 pages)
isbn 978-1718503106, subtitle None, tags ["rust", "beginner"]
{
"title": "Rust Notes",
"pageCount": 42,
"isbn13": "000-0000000000",
"subtitle": "Level 2",
"tags": []
}
error: invalid type: string "many", expected u32 at line 1 column 37
"ferris" knows 2 languages
name as &str: Some("ferris")
missing key gives: null
Printing a Value with {} produces JSON, which is why the name appears with
quotes; .as_str() gets the plain text. Indexing a Value with a missing key
returns Value::Null rather than panicking.
Prefer typed structs for data you understand: the compiler then checks every
field access. Use Value for truly free-form JSON.
Common mistakes
- Forgetting the
derivefeature on serde:error: cannot find derive macro `Deserialize` in this scope. Addfeatures = ["derive"]. - Not telling
from_strwhat to parse into:let book = serde_json::from_str(input)?; println!("{}", book.title);fails witherror[E0282]: type annotations needed. Annotate the variable (let book: Book) or usefrom_str::<Book>. - Using a plain field for optional data. If the key can be absent, a
Stringfield givesmissing field `subtitle`at runtime; make itOption<String>or add#[serde(default)]. - Mismatched naming conventions. JSON APIs often use
camelCase; without#[serde(rename_all = "camelCase")]apage_countfield won't match"pageCount". - Calling
.unwrap()onfrom_strfor input from users or the network. Bad JSON is a normal error — return it with?or report it.
Practice
Define #[derive(Serialize, Deserialize, Debug)] struct Task { id: u32, title: String, done: bool }. Parse the JSON array
[{"id":1,"title":"write lesson","done":true},{"id":2,"title":"review","done":false}]
into a Vec<Task>, print how many tasks are not done, mark them all done, and
print the vector back as compact JSON.
Show solution
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Task {
id: u32,
title: String,
done: bool,
}
fn main() -> Result<(), serde_json::Error> {
let input = r#"[{"id":1,"title":"write lesson","done":true},{"id":2,"title":"review","done":false}]"#;
let mut tasks: Vec<Task> = serde_json::from_str(input)?;
let open = tasks.iter().filter(|t| !t.done).count();
println!("{open} task(s) not done");
for task in &mut tasks {
task.done = true;
}
println!("{}", serde_json::to_string(&tasks)?);
Ok(())
}
1 task(s) not done
[{"id":1,"title":"write lesson","done":true},{"id":2,"title":"review","done":true}]