Parse JSON in Rust with serde_json

serde_json is the standard crate for parsing JSON in Rust, with 200M+ downloads on crates.io. Add serde and serde_json to Cargo.toml and you can deserialize any JSON string into a typed Rust struct or an untyped Value enum in one function call — serde_json::from_str() for strings, serde_json::from_reader() for files and streams. Deserialization with typed structs is typically 2–5× faster than using Value due to fewer allocations, and the derive macro generates all the glue code at compile time with zero boilerplate.

Validate your JSON before parsing it in Rust.

Open JSON Formatter

Add serde_json to Cargo.toml

JSON support in Rust comes from two cooperating crates. Add both to your [dependencies] section:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

serde is the serialization framework — it defines the Serialize and Deserialize traits and provides the #[derive(Deserialize)] and #[derive(Serialize)] procedural macros via the features = ["derive"] flag. serde_json is the JSON-specific format implementation — it translates between JSON text and the serde data model. You need both: serde defines the interface, serde_json provides the JSON backend. Run cargo build after updating Cargo.toml and Cargo will download and compile both crates.

Parse into an untyped Value

When the JSON structure is unknown at compile time — or when you need to inspect arbitrary JSON — use serde_json::Value. It is an enum with six variants that mirror the six JSON types:

JSON typeserde_json::Value variantInner type
objectValue::ObjectMap<String, Value>
arrayValue::ArrayVec<Value>
stringValue::StringString
numberValue::NumberNumber (i64, u64, or f64 internally)
booleanValue::Boolbool
nullValue::Null
use serde_json::Value;

fn main() {
    let json = r#"
    {
        "status": "ok",
        "user": {
            "id": 42,
            "name": "Alice",
            "roles": ["admin", "editor"]
        },
        "score": 98.5,
        "active": true
    }
    "#;

    // Parse into an untyped Value
    let v: Value = serde_json::from_str(json).unwrap();

    // Access fields with indexing — returns &Value::Null for missing keys
    println!("{}", v["status"]);              // "ok"
    println!("{}", v["user"]["name"]);        // "Alice"
    println!("{}", v["user"]["id"]);          // 42
    println!("{}", v["user"]["roles"][0]);    // "admin"

    // Extract Rust types with .as_str(), .as_u64(), .as_bool(), etc.
    let name = v["user"]["name"].as_str().unwrap_or("unknown");
    let id   = v["user"]["id"].as_u64().unwrap_or(0);
    let active = v["active"].as_bool().unwrap_or(false);
    println!("User {id}: {name} (active: {active})");

    // Iterate over an array
    if let Some(roles) = v["user"]["roles"].as_array() {
        for role in roles {
            println!("role: {}", role.as_str().unwrap_or(""));
        }
    }

    // Check the variant explicitly
    match &v["score"] {
        Value::Number(n) => println!("score is a number: {n}"),
        Value::Null      => println!("score is null"),
        other            => println!("unexpected: {other}"),
    }
}

Indexing a Value with a string key or integer index never panics — it returns &Value::Null for missing keys or out-of-bounds indices. The .as_str(), .as_u64(), and .as_bool() methods return Option<T>, so combine them with .unwrap_or() or a match to handle missing values safely. Note that Value::Number stores integers as either i64 or u64 and floats as f64 internally; call .as_i64(), .as_u64(), or .as_f64() depending on your expected range.

Parse into a typed struct with #[derive(Deserialize)]

For JSON with a known shape, derive Deserialize on a Rust struct. The macro generates a Deserialize implementation at compile time — no runtime reflection, no allocations beyond the data itself. This is the idiomatic and recommended approach for any JSON you control or whose schema you know.

use serde::Deserialize;

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

fn main() {
    let json = r#"{"id":42,"name":"Alice","email":"alice@example.com","active":true}"#;

    // serde_json::from_str returns Result<T, serde_json::Error>
    let user: User = serde_json::from_str(json).unwrap();
    println!("{:?}", user);
    // User { id: 42, name: "Alice", email: "alice@example.com", active: true }

    println!("Name: {}", user.name);
    println!("Email: {}", user.email);
}


// ── Nested structs ────────────────────────────────────────────────────────────
#[derive(Deserialize, Debug)]
struct Address {
    street: String,
    city: String,
    country: String,
}

#[derive(Deserialize, Debug)]
struct Person {
    id: u64,
    name: String,
    address: Address,
    tags: Vec<String>,
}

fn parse_person() {
    let json = r#"
    {
        "id": 1,
        "name": "Bob",
        "address": {
            "street": "123 Main St",
            "city": "Springfield",
            "country": "US"
        },
        "tags": ["developer", "rustacean"]
    }
    "#;

    let person: Person = serde_json::from_str(json).unwrap();
    println!("{} lives in {}", person.name, person.address.city);
    println!("Tags: {:?}", person.tags);
}


// ── Array of structs ──────────────────────────────────────────────────────────
fn parse_array() {
    let json = r#"
    [
        {"id": 1, "name": "Alice", "email": "alice@example.com", "active": true},
        {"id": 2, "name": "Bob",   "email": "bob@example.com",   "active": false}
    ]
    "#;

    let users: Vec<User> = serde_json::from_str(json).unwrap();
    println!("Parsed {} users", users.len());
    for u in &users {
        println!("  {} ({})", u.name, u.email);
    }
}

Typed structs are preferred over Value for known schemas because the compiler enforces correctness: a missing required field or a type mismatch surfaces as a serde_json::Error at deserialization time, not a silent None buried deep in business logic. IDE autocompletion works fully on struct fields — it does not on Value indices.

Handle optional and nullable fields

Wrap any field that might be absent or null in Option<T>. serde_json maps both a missing key and a JSON null value to None. For fields that are always present in the JSON but use a different key name, use the #[serde(rename)] or #[serde(rename_all)] attributes.

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct ApiResponse {
    id: u64,
    name: String,

    // Option<T> — None if key is absent or value is null
    email: Option<String>,
    phone: Option<String>,

    // #[serde(default)] — use T::default() if key is absent
    // For Option<T>, default is None; for bool, false; for u64, 0
    #[serde(default)]
    score: u64,

    // #[serde(default = "default_role")] — call a function for the default
    #[serde(default = "default_role")]
    role: String,

    // #[serde(rename)] — map a camelCase JSON key to a snake_case Rust field
    #[serde(rename = "isActive")]
    is_active: bool,

    // #[serde(rename = "createdAt")] — exact key name mapping
    #[serde(rename = "createdAt")]
    created_at: String,
}

fn default_role() -> String {
    "viewer".to_string()
}

fn main() {
    let json = r#"
    {
        "id": 99,
        "name": "Carol",
        "phone": null,
        "isActive": true,
        "createdAt": "2026-05-11T09:00:00Z"
    }
    "#;

    let resp: ApiResponse = serde_json::from_str(json).unwrap();
    println!("email: {:?}", resp.email);       // None  (key absent)
    println!("phone: {:?}", resp.phone);       // None  (value null)
    println!("score: {}", resp.score);         // 0     (default)
    println!("role: {}", resp.role);           // viewer (default_role())
    println!("active: {}", resp.is_active);    // true
    println!("created: {}", resp.created_at); // 2026-05-11T09:00:00Z
}


// ── rename_all for bulk conversion ────────────────────────────────────────────
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Order {
    order_id: u64,       // maps from "orderId"
    customer_name: String, // maps from "customerName"
    total_amount: f64,   // maps from "totalAmount"
    is_paid: bool,       // maps from "isPaid"
}

fn parse_order() {
    let json = r#"
    {"orderId":100,"customerName":"Alice","totalAmount":49.95,"isPaid":true}
    "#;

    let order: Order = serde_json::from_str(json).unwrap();
    println!("{}: ${}", order.customer_name, order.total_amount);
}

#[serde(rename_all = "camelCase")] accepts several conventions: "camelCase", "PascalCase", "SCREAMING_SNAKE_CASE", and "kebab-case". Use it on the struct to convert all fields at once rather than annotating each one individually.

Read JSON from a file

Use serde_json::from_reader() to parse JSON directly from a file handle without first loading the entire contents into a String. Wrapping the file in BufReader reduces system calls by buffering reads — this is especially important for large files.

use std::fs::File;
use std::io::BufReader;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    host: String,
    port: u16,
    debug: bool,
    max_connections: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Open the file — returns std::io::Error on failure
    let file = File::open("config.json")?;

    // Wrap in BufReader for better I/O performance
    let reader = BufReader::new(file);

    // Parse directly from the reader — no intermediate String allocation
    let config: Config = serde_json::from_reader(reader)?;

    println!("Host: {}:{}", config.host, config.port);
    println!("Debug: {}", config.debug);
    println!("Max connections: {}", config.max_connections);

    Ok(())
}


// ── Array of objects from a file ──────────────────────────────────────────────
#[derive(Deserialize, Debug)]
struct Product {
    id: u64,
    name: String,
    price: f64,
}

fn load_products(path: &str) -> Result<Vec<Product>, Box<dyn std::error::Error>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let products: Vec<Product> = serde_json::from_reader(reader)?;
    Ok(products)
}

from_reader is streaming: it parses the JSON incrementally as bytes arrive from the file rather than reading everything into memory first. For multi-gigabyte JSON files, this keeps memory usage near-constant. Use from_str or from_slice only when you already have the data in memory (e.g., from an HTTP response body).

Serialize Rust structs to JSON

Derive Serialize alongside Deserialize to enable round-trip conversion. serde_json::to_string() produces compact JSON; serde_json::to_string_pretty() produces human-readable JSON with two-space indentation. The json!() macro builds a Value inline using JSON-like syntax without defining a struct.

use serde::{Deserialize, Serialize};
use serde_json::json;

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

fn main() -> Result<(), serde_json::Error> {
    let user = User {
        id: 42,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        active: true,
    };

    // Compact JSON — single line
    let compact = serde_json::to_string(&user)?;
    println!("{}", compact);
    // {"id":42,"name":"Alice","email":"alice@example.com","active":true}

    // Pretty-printed JSON — two-space indentation
    let pretty = serde_json::to_string_pretty(&user)?;
    println!("{}", pretty);
    // {
    //   "id": 42,
    //   "name": "Alice",
    //   "email": "alice@example.com",
    //   "active": true
    // }

    // Serialize a Vec<T>
    let users = vec![
        User { id: 1, name: "Alice".into(), email: "alice@example.com".into(), active: true },
        User { id: 2, name: "Bob".into(),   email: "bob@example.com".into(),   active: false },
    ];
    println!("{}", serde_json::to_string_pretty(&users)?);


    // ── json!() macro — build Value without a struct ──────────────────────────
    // NOTE: json!() is evaluated at RUNTIME, not compile time
    let name = "Alice";
    let scores = vec![95, 87, 100];

    let payload = json!({
        "name": name,
        "scores": scores,
        "meta": {
            "version": 1,
            "source": "internal"
        }
    });

    println!("{}", serde_json::to_string_pretty(&payload)?);

    // Serialize the Value to a writer (e.g., stdout, file, network socket)
    serde_json::to_writer_pretty(std::io::stdout(), &payload)?;

    Ok(())
}

to_writer() and to_writer_pretty() write directly to any type implementing std::io::Write — including std::fs::File, Vec<u8>, and network streams — without an intermediate String allocation. Prefer them when writing large JSON payloads to files or HTTP responses.

Error handling

serde_json::from_str() returns Result<T, serde_json::Error>. Calling .unwrap() on invalid JSON causes a panic at runtime — always handle the error explicitly in production code. serde_json::Error exposes the error category, line, and column through its API.

use serde::Deserialize;
use serde_json::Error;

#[derive(Deserialize, Debug)]
struct Config {
    host: String,
    port: u16,
}

// ── Pattern 1: match on the Result ───────────────────────────────────────────
fn parse_v1(json: &str) {
    match serde_json::from_str::<Config>(json) {
        Ok(config) => println!("Parsed: {:?}", config),
        Err(e) => {
            // e.is_syntax()    — malformed JSON (missing quote, trailing comma, etc.)
            // e.is_data()      — type mismatch or missing required field
            // e.is_eof()       — input ended before JSON was complete
            // e.is_io()        — I/O error (only possible with from_reader)
            eprintln!("Parse error at line {}, col {}: {}", e.line(), e.column(), e);
        }
    }
}

// ── Pattern 2: ? operator in a Result-returning function ─────────────────────
fn parse_v2(json: &str) -> Result<Config, Error> {
    let config: Config = serde_json::from_str(json)?;
    Ok(config)
}

// ── Pattern 3: Box<dyn Error> for mixed error types ──────────────────────────
use std::fs::File;
use std::io::BufReader;

fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
    let file = File::open(path)?;          // std::io::Error
    let reader = BufReader::new(file);
    let config: Config = serde_json::from_reader(reader)?;  // serde_json::Error
    Ok(config)
}

fn main() {
    // ── SyntaxError — malformed JSON ──────────────────────────────────────────
    let bad_syntax = r#"{"host": "localhost" "port": 8080}"#; // missing comma
    parse_v1(bad_syntax);
    // Parse error at line 1, col 22: expected `,` or `}` at line 1 column 22

    // ── Data error — type mismatch ────────────────────────────────────────────
    let wrong_type = r#"{"host": "localhost", "port": "not-a-number"}"#;
    parse_v1(wrong_type);
    // Parse error at line 1, col 45: invalid type: string "not-a-number",
    //   expected u16 at line 1 column 45

    // ── Data error — missing required field ───────────────────────────────────
    let missing_field = r#"{"host": "localhost"}"#; // "port" is required
    parse_v1(missing_field);
    // Parse error at line 1, col 21: missing field `port` at line 1 column 21

    // ── Success ───────────────────────────────────────────────────────────────
    let good = r#"{"host": "localhost", "port": 8080}"#;
    parse_v1(good);
    // Parsed: Config { host: "localhost", port: 8080 }

    // ── Using ? in a Result-returning context ─────────────────────────────────
    match parse_v2(good) {
        Ok(c) => println!("host={}, port={}", c.host, c.port),
        Err(e) => eprintln!("error: {e}"),
    }
}

Use e.is_syntax() to detect malformed JSON (return a 400 Bad Request), e.is_data() to detect type mismatches or missing fields (log and alert), and e.is_io() to detect underlying I/O failures (retry logic). The ? operator is the idiomatic way to propagate errors up the call stack in any function that returns Result.

Validate your JSON before parsing

Paste your JSON into Jsonic to catch syntax errors before you hit them in Rust.

Open JSON Formatter

Frequently asked questions

What crate should I use for JSON in Rust?

serde_json is the de facto standard with 200M+ downloads on crates.io. Add serde = { version: "1", features: ["derive"] } and serde_json = "1" to your Cargo.toml. The serde framework handles the trait machinery; serde_json provides the JSON format implementation. There are alternatives — simd-json for SIMD-accelerated parsing and json for a minimal dependency — but serde_json is the community standard and works with the broadest ecosystem of crates.

What is the difference between serde_json::Value and a typed struct?

Value is an untyped enum with six variants (Object, Array, String, Number, Bool, Null) useful when the JSON structure is unknown at compile time — for example, a generic JSON proxy or a configuration loader that accepts arbitrary keys. A typed struct with #[derive(Deserialize)] generates compile-time code for zero-overhead deserialization and gives you full type safety, compile-time error detection, and IDE autocompletion. Typed structs are typically 2–5× faster than Value due to fewer allocations, and the Rust compiler will catch field name typos and type mismatches before you ship.

How do I handle missing fields in Rust JSON deserialization?

Use Option<T> for fields that may be absent. serde_json maps a missing JSON key or a JSON null to None. Add #[serde(default)] to use the field type's Default value when the key is absent entirely — for bool that is false, for u64 it is 0, for String it is an empty string. Use #[serde(default = "my_fn")] to call a custom function for a non-default default value, such as returning "viewer" for a role field.

How do I rename a JSON field that doesn't match Rust naming conventions?

Use #[serde(rename = "fieldName")] on the struct field to map a specific JSON key to a Rust field name. For bulk renaming, use #[serde(rename_all = "camelCase")] on the struct itself — it applies snake_case→camelCase conversion to all fields automatically. Options include "camelCase", "PascalCase", "SCREAMING_SNAKE_CASE", and "kebab-case". The rename attributes apply to both serialization and deserialization, so your JSON output uses the same key names as your input.

Can I parse JSON from a file in Rust without reading it all into memory?

Yes. Use serde_json::from_reader() with a std::fs::File or BufReader<File>. The reader-based API streams the file rather than loading it entirely, making it suitable for JSON files larger than available RAM. Wrap the file in BufReader for better performance on most operating systems — it reduces system calls by buffering reads into chunks. Use from_str only when you already have the entire JSON in memory (e.g., from an HTTP response you received as a String).

How do I build a JSON object dynamically in Rust without a struct?

Use the serde_json::json!() macro. It accepts JSON-like syntax and returns a serde_json::Value:

use serde_json::json;

// Variables and expressions can be interpolated directly
let name = "Alice";
let scores = vec![1, 2, 3];

let v = json!({
    "name": name,
    "scores": scores,
    "active": true
});
println!("{}", v);

The json!() macro is evaluated at runtime, not compile time — the values are inserted when the statement executes, not when Cargo compiles your crate. For fully programmatic construction (building keys and values in a loop), use serde_json::Map::new() and insert Value entries directly, then wrap with Value::Object(map). See also parse JSON in Go and parse JSON in C# for comparison with how other statically-typed languages handle dynamic JSON construction.