Skip to content

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.

Inside your project, add both crates. The derive feature lets serde write the translation code for you:

Terminal window
cargo add serde --features derive
cargo add serde_json

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

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
}
▶ Try it Yourself

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.

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 old
▶ Try it Yourself

Two things to notice:

  • r#"..."# is a raw string. Inside it, you can write " freely, which is very handy for JSON.
  • from_str returns a Result, because the text might not be valid JSON. Here main returns Result, so we can use ? to pass any error up.

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 15
Error: invalid type: string "old", expected u32 at line 1 column 27
▶ Try it Yourself

The ::<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 (or null) becomes None.
  • Add #[serde(default)]. A missing key gets the type’s default value: 0, false, an empty String or an empty Vec.
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: [] }
▶ Try it Yourself

Use Option when “no value” means something different from “zero” or “empty”. Use default when the default value is good enough.

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"}
▶ Try it Yourself

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.

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 API
2 tasks left
▶ Try it Yourself

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: 18
Plain text: Lisbon
Wind: null
▶ Try it Yourself

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

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 }
▶ Try it Yourself

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.

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` type

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

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}]
▶ Try it Yourself

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.

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 }
▶ Try it Yourself

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) and serde_json, then #[derive(Serialize, Deserialize)] on your structs.
  • serde_json::to_string / to_string_pretty turn a value into JSON; serde_json::from_str turns JSON into a value.
  • from_str returns a Result. Handle it with ? or match, not .unwrap(), for real input.
  • Use Option or #[serde(default)] for keys that may be missing, and rename / rename_all to match the JSON’s names.
  • Use serde_json::Value and json! when you don’t know the shape. Prefer structs when you do.

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