rustserdejsonparsingconcepts

Working with JSON in Rust: A serde_json Guide

·9 min read·JSON Concepts

JSON in Rust means serde

Rust doesn't have JSON in its standard library — and that's fine, because the ecosystem standardized on serde (serialize/deserialize) and serde_json so thoroughly they're effectively the default. serde's trick is compile-time code generation: you annotate a struct, and the compiler writes fast, correct parsing and serialization for you. The result is JSON handling that's both ergonomic and impossible to get subtly wrong at runtime the way dynamic languages allow.

toml
# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Typed parsing: derive and go

The idiomatic path is a struct with #[derive(Serialize, Deserialize)]. serde maps JSON fields to struct fields by name:

rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u64,
    name: String,
    email: String,
    active: bool,
}

fn main() -> serde_json::Result<()> {
    let data = r#"{"id":1,"name":"Ada","email":"ada@example.com","active":true}"#;
    let user: User = serde_json::from_str(data)?;   // parse
    println!("{}", user.name);

    let back = serde_json::to_string(&user)?;        // serialize
    println!("{}", back);
    Ok(())
}

If the JSON is missing id or id is a string, from_str returns an Err — you handle the failure explicitly instead of discovering undefined three functions later. That's the safety Rust buys you.

Untyped parsing: serde_json::Value

When the shape is unknown or dynamic, parse into serde_json::Value — an enum that can hold any JSON node — and navigate it:

rust
use serde_json::Value;

let v: Value = serde_json::from_str(r#"{"user":{"name":"Ada"}}"#)?;
if let Some(name) = v["user"]["name"].as_str() {
    println!("{name}");
}

Use Value for config you probe dynamically or truly heterogeneous data; prefer typed structs everywhere else, because they document the shape and catch mismatches at parse time.

The attributes you'll actually use

serde's power is in field attributes that bridge idiomatic Rust and real-world JSON:

  • Rename between conventions. JSON APIs use camelCase; Rust uses snake_case:
  • ```rust
  • #[derive(Deserialize)]
  • #[serde(rename_all = "camelCase")]
  • struct Account { user_id: u64, created_at: String } // reads "userId", "createdAt"
  • `
  • Optional fields map to Option<T> — a missing or null value becomes None instead of an error.
  • Defaults with #[serde(default)] fill absent fields.
  • Skip serializing empties with #[serde(skip_serializing_if = "Option::is_none")] to keep output clean.
rust
#[derive(Serialize, Deserialize)]
struct Profile {
    name: String,
    #[serde(default)]
    bio: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    avatar: Option<String>,
}

From a JSON sample to Rust structs

Hand-writing structs for a large API response is tedious and error-prone. Speed it up:

  1. Format the API sample so the shape is clear and you can spot optional fields.
  2. Generate the structs automatically with JSON to Rust — it emits serde-annotated structs, including nested types.
  3. Validate real payloads against the shape when the API is one you don't control, so a silent field change surfaces in tests.

For the same ideas in other languages, see JSON in Python, JSON to Go structs, and JSON to TypeScript.

Frequently asked questions

The serde framework with serde_json. Derive Serialize/Deserialize on a struct for typed handling, or use serde_json::Value for dynamic data. serde generates the parsing code at compile time, so it's fast and type-safe.

Use Value when the JSON shape is unknown, dynamic, or highly heterogeneous. Prefer typed structs whenever you know the shape — they document it, and mismatches fail at parse time rather than silently.

Add #[serde(rename_all = "camelCase")] to the struct, or #[serde(rename = "...")] on individual fields. serde translates between the JSON keys and your idiomatic Rust field names automatically.

Model them as Option<T> — a missing or null value deserializes to None instead of erroring. Combine with #[serde(default)] to fill absent fields and skip_serializing_if to omit empties on output.

Try JSON to Rust

Generate serde-annotated Rust structs from any JSON object automatically.