Rust JSON with Serde
JSON is everywhere: config files, web APIs, saved game data. In Rust, almost everyone uses two crates to work with it. serde (short for serialize / deserialize) knows how to turn Rust values into data and back. serde_json teaches it the JSON format.
Think of serde as a translator. You describe your data once, as a normal Rust struct, and serde translates it to JSON and back for you. You never write the JSON text by hand, and you never pick JSON apart character by character.
Setting up
Section titled “Setting up”Inside your project, add both crates. The derive feature lets serde write the translation code for you:
cargo add serde --features derivecargo add serde_jsonYour Cargo.toml now has:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"If Cargo.toml and cargo add are new to you, read Cargo & Crates first. The Rust Playground already has both crates, so every “Try it Yourself” button on this page works without setup.
Struct to JSON
Section titled “Struct to JSON”Add #[derive(Serialize)] to a struct, and serde_json::to_string can turn it into JSON text:
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);}Output:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string gives you compact JSON, which is good for sending over a network. to_string_pretty adds line breaks and spaces, which is nicer for files people will read. The keys keep the same order as the fields in your struct.
JSON to struct
Section titled “JSON to struct”Going the other way is called deserializing. Add Deserialize to the derive list, then call serde_json::from_str. You tell it which type you want with a type annotation:
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!("{} is {} years old", user.name, user.age); Ok(())}Output:
User { name: "Ana", age: 28, admin: false }Ana is 28 years oldTwo things to notice:
r#"..."#is a raw string. Inside it, you can write"freely, which is very handy for JSON.from_strreturns aResult, because the text might not be valid JSON. HeremainreturnsResult, so we can use?to pass any error up.
When the JSON is wrong
Section titled “When the JSON is wrong”Real JSON is often not what you expect. A field is missing, or a number arrives as text. serde checks everything and gives you a clear error instead of crashing. Use match to handle it:
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": "old"}"#, ];
for text in inputs { match serde_json::from_str::<User>(text) { Ok(user) => println!("OK: {:?}", user), Err(e) => println!("Error: {}", e), } }}Output:
OK: User { name: "Ana", age: 28 }Error: missing field `age` at line 1 column 15Error: invalid type: string "old", expected u32 at line 1 column 27The ::<User> part is called the “turbofish”. It is another way to say which type you want, useful when there is no let user: User to put the annotation on.
Missing keys: Option and #[serde(default)]
Section titled “Missing keys: Option and #[serde(default)]”What if a field is really optional? You have two good choices:
- Make the field an
Option. A missing key (ornull) becomesNone. - Add
#[serde(default)]. A missing key gets the type’s default value:0,false, an emptyStringor an emptyVec.
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": "Loves 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);}Output:
Profile { name: "Ana", bio: Some("Loves Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }Use Option when “no value” means something different from “zero” or “empty”. Use default when the default value is good enough.
Renaming fields
Section titled “Renaming fields”Rust uses snake_case for field names, but many web APIs use camelCase, and some use names that are not valid Rust at all (like type). You don’t have to give up nice Rust names. Use rename_all for the whole struct, and rename for a single field:
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() { // the kind of response a web API might send back let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{} (id {}) has a {} account", profile.display_name, profile.user_id, profile.account_type);
// serializing uses the same names, so the JSON round-trips println!("{}", serde_json::to_string(&profile).unwrap());}Output:
Ana Silva (id 42) has a premium account{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}By default, serde simply ignores JSON keys that your struct doesn’t have. So you only need to describe the fields you care about, even if the API sends fifty.
Lists of things: Vec of structs
Section titled “Lists of things: Vec of structs”A JSON array becomes a Vec. Structs can also contain other structs and vectors, so you can describe deep JSON just by nesting types:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "Learn serde", "done": true}, {"title": "Write a config file", "done": false}, {"title": "Call an API", "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!("{} tasks left", left);}Output:
[x] Learn serde[ ] Write a config file[ ] Call an API2 tasks leftUntyped JSON: Value and json!
Section titled “Untyped JSON: Value and json!”Sometimes you don’t know the shape of the JSON ahead of time, or you only need one small piece of it. For that, use serde_json::Value. It can hold any JSON: an object, an array, a string, a number, a bool, or null. The json! macro builds a Value with JSON-like syntax:
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "Lisbon", "temps": [18, 21, 19], "sunny": true });
println!("City: {}", data["city"]); println!("First temp: {}", data["temps"][0]);
// as_str() gives you a plain &str instead of a JSON string if let Some(city) = data["city"].as_str() { println!("Plain text: {}", city); }
// a missing key gives Null instead of crashing println!("Wind: {}", data["wind"]);}Output:
City: "Lisbon"First temp: 18Plain text: LisbonWind: nullNotice that printing data["city"] shows quotes: it is still a JSON string. Use .as_str(), .as_i64(), or .as_bool() to get plain Rust values. Each returns an Option, because the value might be a different type.
Value is flexible, but you lose the compiler’s help. Prefer a struct whenever you know the shape.
Reading and writing a JSON file
Section titled “Reading and writing a JSON file”Here is a real-world example: saving app settings to config.json and loading them again. It combines this page with File I/O. std::fs::write saves the text, and std::fs::read_to_string loads it back:
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, };
// save let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// load let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("Loaded: {:?}", loaded); Ok(())}Output:
Loaded: Config { username: "ana", theme: "dark", font_size: 14 }Why Box<dyn Error>? This main can fail in two different ways: a file error (std::io::Error) or a JSON error (serde_json::Error). Box<dyn Error> means “any kind of error”, so ? works for both. The Custom Errors page explains this in more detail.
Common mistakes
Section titled “Common mistakes”Forgetting to derive Deserialize. If your struct only derives Serialize, you can write JSON but not read it. The error is long, but the important lines are:
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` typeThe fix: #[derive(Serialize, Deserialize)]. If instead you see “cannot find derive macro Serialize”, you added serde without the derive feature. Run cargo add serde --features derive.
Using .unwrap() on data you don’t control. It is fine in small examples, but JSON from a user or a website can be broken. A bad file will crash your program with a panic. Use ? or match as shown above.
Expecting data["name"] to print without quotes. A Value prints as JSON. Call .as_str() to get plain text.
Practice
Section titled “Practice”1. Save a shopping list
Section titled “1. Save a shopping list”Make a struct Item with a name (String) and a qty (u32). Put two items in a Vec, turn the whole vector into JSON with one call, and print it.
Hint: only the struct needs #[derive(Serialize)]. A Vec of serializable things is serializable too, and it becomes a JSON array.
Show solution
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("apples"), qty: 3 }, Item { name: String::from("bread"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}Output:
[{"name":"apples","qty":3},{"name":"bread","qty":1}]serde already knows how to handle Vec, String and numbers, so deriving Serialize on Item is all it takes. Try to_string_pretty to see the same list spread over several lines.
2. Settings with defaults
Section titled “2. Settings with defaults”Read this JSON into a Settings struct: {"siteTitle": "My blog", "postsPerPage": 5}. The struct should have site_title, posts_per_page, and a theme that may be missing. Then read {"siteTitle": "Notes"} too, where posts_per_page should fall back to 0. Print both with {:?}.
Hint: you need #[serde(rename_all = "camelCase")] on the struct, Option<String> for theme, and #[serde(default)] on posts_per_page.
Show solution
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": "My blog", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "Notes"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Output:
Settings { site_title: "My blog", posts_per_page: 5, theme: None }Settings { site_title: "Notes", posts_per_page: 0, theme: None }rename_all maps siteTitle to site_title and postsPerPage to posts_per_page. Neither JSON has a theme, so it’s None both times, and the missing postsPerPage in the second one becomes 0. Remove #[serde(default)] and the second unwrap() panics with “missing field postsPerPage”. The .unwrap() is fine here because we wrote the JSON ourselves; for real input, use ? or match.
- Add
serde(with--features derive) andserde_json, then#[derive(Serialize, Deserialize)]on your structs. serde_json::to_string/to_string_prettyturn a value into JSON;serde_json::from_strturns JSON into a value.from_strreturns aResult. Handle it with?ormatch, not.unwrap(), for real input.- Use
Optionor#[serde(default)]for keys that may be missing, andrename/rename_allto match the JSON’s names. - Use
serde_json::Valueandjson!when you don’t know the shape. Prefer structs when you do.
Quick check
Section titled “Quick check”Quick check
1. Which derive lets you turn JSON text into your struct?
2. A JSON object might not contain the key bio. What is a good type for that field?
3. Your struct has a field display_name, but the JSON uses displayName. What is the simplest fix for the whole struct?
4. What does serde do by default with JSON keys that your struct does not have?
Score: 0 / 4
Read more
Section titled “Read more”- serde.rs — the official Serde guide: an overview of how serde works, with examples for many data formats.
- Container attributes — serde.rs: options for a whole struct or enum, like
rename_allanddeny_unknown_fields. - Field attributes — serde.rs: options for single fields, like
rename,default,skipandskip_serializing_if. serde_json— docs.rs: the crate’s front page, with examples of typed and untyped JSON and thejson!macro.serde_json::Value— docs.rs: every method for reading untyped JSON, likeas_str,as_i64andget.