Skip to content

Expert · Real projects: CLI, web and WebAssembly · topic 44 of 50 · 25 min

Serde and JSON

Explanation

serde is Rust's standard way to convert data to and from formats like JSON. Add #[derive(Serialize, Deserialize)] to a struct, then use serde_json::to_string to write JSON and serde_json::from_str to read it into a typed struct. Attributes such as #[serde(rename_all = "camelCase")] match other naming styles, and Option fields handle missing values.

Why serde?

Programs constantly exchange data: web APIs send JSON, config files use TOML or YAML, caches store binary data. Serialisation means turning a value in memory into text or bytes; deserialisation is the reverse. serde (SERialise/DEserialise) is the crate almost every Rust project uses for this. It is fast, and it turns messy text into typed structs, so the rest of your code never deals with raw strings.

bash
cargo add serde --features derive
cargo add serde_json
serde provides the traits and derives; serde_json provides the JSON format.

Struct to JSON and back

rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Order {
    id: u32,
    item: String,
    qty: u32,
    paid: bool,
}

fn main() -> Result<(), serde_json::Error> {
    let o = Order { id: 7, item: String::from("thali"), qty: 2, paid: true };

    let text = serde_json::to_string(&o)?;
    println!("{text}");   // {"id":7,"item":"thali","qty":2,"paid":true}

    let back: Order = serde_json::from_str(r#"{"id":8,"item":"lassi","qty":1,"paid":false}"#)?;
    println!("{:?}", back);
    Ok(())
}
r#"..."# is a raw string: quotes inside do not need escaping. to_string_pretty gives indented JSON.
Think of it like this: serde is like a translator at a passport office. Your struct is the official form with fixed boxes; JSON is what applicants say in many different ways. The translator fills the form correctly, refuses an application with a missing box, and writes the form back out in any language (JSON, TOML, YAML) the next office needs.

Serialize and Deserialize are traits (module 7). The derive writes their code for you from the struct's fields at compile time, so there is no slow run-time inspection of your types. The same derive works for every format crate: serde_json for JSON, toml for TOML files, and others, which is why learning serde once pays off everywhere.

from_str checks everything: if a field is missing, has the wrong type, or the JSON is broken, you get an Err with a clear message such as "missing field `qty` at line 1 column 30". Once you have an Order, every field is guaranteed to be valid.

Matching other naming styles

JavaScript APIs usually use camelCase (orderId), while Rust uses snake_case (order_id). Attributes tell serde how to map names, so your Rust code stays idiomatic.

rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Payment {
    order_id: u32,              // JSON: "orderId"
    #[serde(rename = "txnRef")]
    reference: String,          // JSON: "txnRef"
    upi_id: Option<String>,     // may be missing or null
    #[serde(default)]
    retries: u32,               // missing -> 0
}
Option fields accept null or a missing key; #[serde(default)] uses the type's default value.

Enums and nested data

rust
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
enum Status { Placed, Shipped, Delivered }

#[derive(Serialize, Deserialize, Debug)]
struct Tracking {
    id: u32,
    status: Status,             // "shipped"
    stops: Vec<String>,         // JSON array
}
Vec becomes a JSON array, nested structs become nested objects, simple enum variants become strings.

Unknown shape: serde_json::Value

When you do not know the structure in advance, read into serde_json::Value, a type that can hold any JSON. Prefer typed structs whenever you can; they catch mistakes much earlier.

rust
let v: serde_json::Value = serde_json::from_str(r#"{"city":"Pune","temp":31}"#)?;
println!("{}", v["city"]);          // "Pune"
println!("{}", v["temp"].as_i64().unwrap_or(0) + 1);   // 32
Indexing a Value with a missing key gives Null rather than panicking.

Common mistakes

  • Forgetting the derive feature: "cannot find derive macro Serialize". Use cargo add serde --features derive.
  • Making every field Option "to be safe". Then the compiler cannot help you; only use Option for truly optional data.
  • Using f64 for money in JSON. Send paise as an integer or a string.
  • Ignoring the error from from_str. Show it: it says exactly which field and position failed.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Serde and JSON · 1/6

Serialise ⇄ Deserialise

serde converts Rust values to and from formats like JSON. Almost every Rust project uses it.

Prefer a video by a teacher? Find free videos on “rust serde json serialize deserialize tutorial” on YouTube ↗

In real life: Reading a weather API response

A farming advice app calls a weather API that returns JSON with camelCase names and some optional fields. The Rust backend defines a Forecast struct with rename_all = "camelCase" and Option for rain probability. If the API changes a field's type one day, deserialisation fails with a clear error in the logs instead of silently showing farmers wrong numbers.

rust
use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Forecast {
    district: String,
    max_temp_c: i32,
    rain_chance: Option<u8>,
}

fn main() {
    let json = r#"{"district":"Nashik","maxTempC":33,"rainChance":null}"#;
    match serde_json::from_str::<Forecast>(json) {
        Ok(f) => println!("{} {} {:?}", f.district, f.max_temp_c, f.rain_chance),
        Err(e) => eprintln!("bad data: {e}"),
    }
}
Prints: Nashik 33 None

Practice (0/3 done)

Task 1 · Question

What does this print? #[derive(Serialize)] struct P { name: String, age: u8 } let p = P { name: "Ira".into(), age: 9 }; println!("{}", serde_json::to_string(&p).unwrap());

Task 2 · Question

The JSON has "orderId" but your field is order_id. What fixes it?

Task 3 · Practice

Define structs for a train booking JSON: {"pnr":"4512378901","trainNo":12951,"passengers":[{"name":"Asha","age":34,"berth":"LB"},{"name":"Om","age":8,"berth":null}]}. Deserialize it, print each passenger, add one passenger, and serialize it back with to_string_pretty.

Quick quiz

Question 1

1.What does deserialisation mean?

Question 2

2.A JSON field may be null or missing. Which field type fits?

Question 3

3.What does from_str return when a required field is missing?

Question 4

4.Which feature must be enabled on serde for #[derive(Serialize)]?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does deserialisation mean?

Answer: Turning text or bytes back into a typed value

Deserialise = read external data into your types.

A JSON field may be null or missing. Which field type fits?

Answer: Option<String>

Option maps null and missing keys to None.

What does from_str return when a required field is missing?

Answer: An Err describing the missing field

serde reports exactly what is wrong.

Which feature must be enabled on serde for #[derive(Serialize)]?

Answer: derive

cargo add serde --features derive.

What does JsonSerializer.Deserialize<T> throw for text that is not valid JSON?Related

Answer: JsonException

Invalid JSON raises JsonException.

From Files and JSON with System.Text.Json

After let b = Student { roll: 2, ..a }; where a has String fields, what is true?Related

Answer: a's String fields were moved into b

Struct update syntax moves non-Copy fields.

From Structs: your own data types

What happens to a JSON key that has no matching struct field?Related

Answer: It is ignored, unless the Decoder uses DisallowUnknownFields

By default unknown keys are skipped.

From JSON with encoding/json

What can an enum variant hold in Rust?Related

Answer: Any data: none, a tuple of values, or named fields

Rust variants can be unit-like, tuple-like or struct-like.

From Enums and Option

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Real projects: CLI, web and WebAssembly” before its exam: Rust for the web with axum, Rust and WebAssembly.
  2. Module 11 exam: Real projects · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Rust careers and interviews. 50 topics left in the course before the final exam.

← Previous: Building a CLI tool