Skip to content
AgileLogics
7 min read

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 derive feature on serde: error: cannot find derive macro `Deserialize` in this scope. Add features = ["derive"].
  • Not telling from_str what to parse into: let book = serde_json::from_str(input)?; println!("{}", book.title); fails with error[E0282]: type annotations needed. Annotate the variable (let book: Book) or use from_str::<Book>.
  • Using a plain field for optional data. If the key can be absent, a String field gives missing field `subtitle` at runtime; make it Option<String> or add #[serde(default)].
  • Mismatched naming conventions. JSON APIs often use camelCase; without #[serde(rename_all = "camelCase")] a page_count field won't match "pageCount".
  • Calling .unwrap() on from_str for 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}]