How to Parse JSON in Rust

Parse JSON in Rust using the serde_json crate and from_str function to convert strings into structs or Value types.

When JSON meets Rust

You're building a Rust app that talks to an API. You fetch a response body, and it's a blob of JSON. In Python, you'd call json.loads and get a dictionary. In JavaScript, JSON.parse gives you an object. Rust doesn't have a built-in JSON parser in the standard library. You need a crate. The ecosystem standard is serde_json, backed by the serialization framework serde. This setup turns JSON into Rust structs with zero runtime overhead, but it demands you define your data shape upfront.

The compiler will reject your code if the JSON structure doesn't match your Rust types. This feels like friction at first. You spend time writing structs and attributes instead of parsing immediately. That friction is the safety net. It catches mismatched fields, wrong types, and missing data before your program runs. You trade a few lines of setup for guarantees that your data is valid.

The customs officer analogy

JSON is just text. Rust needs types. Parsing is the act of turning that text into values your program can use. Think of serde as a strict customs officer. Your JSON is the cargo arriving at the border. Your Rust struct is the declaration form. The officer checks every item in the cargo against the form. If the JSON has a field the struct doesn't expect, or a type that doesn't match, the officer rejects the shipment.

This strictness prevents bugs where you accidentally treat a string as a number. It also means you have to define your forms carefully. The officer doesn't guess. If the form says "count" must be a number, and the cargo contains a string, the shipment stops. In Rust terms, the deserialization fails and returns an error. You handle the error, or the program stops. There is no silent coercion.

Minimal parsing with Value

When you don't know the JSON shape, or the shape changes dynamically, parse into serde_json::Value. This type represents any JSON value: object, array, string, number, boolean, or null. It behaves like a dictionary in Python or an object in JavaScript.

Add serde_json to your dependencies. The convention is to include serde with the derive feature as well. You'll need serde for struct parsing later, and the community expects both crates to be present.

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

Parse a string into Value and access fields by index.

use serde_json;

fn main() {
    // Raw JSON string. The r#"..."# syntax avoids escaping quotes.
    let json_input = r#"{"status": "ok", "count": 42, "tags": ["rust", "json"]}"#;

    // Parse into serde_json::Value for dynamic access.
    // This allocates a tree structure representing the JSON.
    let data: serde_json::Value = serde_json::from_str(json_input).unwrap();

    // Indexing returns a Value. Use as_str() or as_i64() to get typed data.
    // If the key is missing, indexing returns Value::Null, not an error.
    println!("Status: {}", data["status"]);
    println!("Count: {}", data["count"].as_i64().unwrap_or(0));

    // Iterate over an array. Check is_array() before indexing.
    if let Some(tags) = data["tags"].as_array() {
        for tag in tags {
            println!("Tag: {}", tag);
        }
    }
}

Value is flexible but loses type safety. You have to check types at runtime. Calling as_i64() on a string returns None. Forgetting to check leads to unwrap() panics. Use Value for quick scripts or when the JSON structure is truly unknown. For production code, define structs.

Define your structs. The compiler will save you from runtime panics.

How from_str works

serde_json::from_str takes a &str and returns a Result<T, serde_json::Error>. The Result type forces you to handle parsing failures. If the JSON is malformed, you get an error instead of a crash. The unwrap() call panics if parsing fails. In production code, handle the error with match or the ? operator.

The function works by reading the string character by character. It builds the target type based on the JSON tokens. When parsing into Value, it constructs an enum tree. When parsing into a struct, serde generates code that maps JSON keys to struct fields. This generated code runs at runtime. The generation happens at compile time, so there is no reflection overhead.

The Result contains a serde_json::Error on failure. This error provides a line and column number, plus a human-readable message. Print the error to debug malformed JSON. Never swallow errors in production. Log them and return a meaningful response.

Handle the Result. Unwrapping in production is a promise you'll regret.

Realistic parsing with structs

Real applications parse JSON into structs. This gives you type safety and IDE support. Define a struct that matches the JSON shape. Add #[derive(Deserialize)] to generate the parsing code.

JSON APIs often use camelCase keys. Rust uses snake_case. Use #[serde(rename_all = "camelCase")] to handle the mismatch automatically. This attribute tells serde to map user_name in Rust to userName in JSON.

use serde::Deserialize;

/// Represents the shape of the JSON response we expect.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    status: String,
    count: u32,
    // Optional field. Rust requires Option for missing JSON keys.
    // If the key is missing, the field becomes None.
    message: Option<String>,
    // Nested object. Serde handles recursion automatically.
    metadata: Metadata,
}

#[derive(Deserialize, Debug)]
struct Metadata {
    version: u32,
    // Default value. If missing, uses the default for the type.
    #[serde(default)]
    timestamp: u64,
}

fn main() {
    let json = r#"{"status": "ok", "count": 42, "metadata": {"version": 1}}"#;

    // Parse into ApiResponse. The type annotation is crucial.
    // Serde generates code to map JSON keys to struct fields.
    let response: ApiResponse = serde_json::from_str(json).unwrap();

    println!("{:?}", response);
}

The #[serde(default)] attribute sets a field to its default value if the key is missing. For Option, the default is None. For numbers, it's zero. Use default when you want a specific fallback. Use Option when the absence of data is meaningful.

Convention aside: The community prefers Option for truly optional fields. Use #[serde(default)] only when you have a sensible default value. Mixing both can confuse readers. Stick to one pattern per field.

Check for Value::Null. Missing keys hide in plain sight.

Pitfalls and compiler errors

Forgetting #[derive(Deserialize)] breaks compilation. The compiler rejects the code with E0277: the trait bound ApiResponse: Deserialize<'_> is not satisfied. Add the derive macro to fix this. The macro generates the implementation. Without it, serde doesn't know how to parse the struct.

JSON keys are case-sensitive. name in JSON must match name in the struct. If the API returns Name, parsing fails. Use rename_all or #[serde(rename = "Name")] to fix mismatches. Don't rely on case-insensitive matching. It slows down parsing and hides bugs.

Using from_str with byte data causes type errors. from_str expects &str. If you have Vec<u8> from a file or network request, use serde_json::from_slice. Passing bytes to from_str requires converting to a string first, which allocates memory. from_slice parses directly from bytes. It is faster and avoids unnecessary allocations.

Indexing Value with a missing key returns Value::Null. This is not an error. It is a valid JSON value. Code that assumes data["missing"] panics will crash. Check for null explicitly. Use data.get("key") which returns Option<&Value>. This forces you to handle the missing case.

// Safe access pattern for Value.
if let Some(count) = data.get("count").and_then(|v| v.as_i64()) {
    println!("Count is {}", count);
} else {
    println!("Count missing or invalid");
}

Use from_slice for bytes. The type system knows the difference.

Choosing the right parser

Use serde_json::Value when the JSON structure is dynamic or unknown at compile time. Use serde_json::Value for quick scripts where defining structs is overkill. Use serde_json::Value when you need to inspect arbitrary JSON without a schema.

Use a struct with #[derive(Deserialize)] when you know the JSON shape and want type safety. Use structs for production code to catch mismatches at compile time. Use structs when you want IDE autocomplete and refactoring support.

Use serde_json::from_slice when you have JSON data as a byte slice. Use from_slice for file contents or HTTP response bodies stored as bytes. Use from_slice to avoid allocating a string intermediate.

Use serde_json::from_reader when parsing from a stream. Use from_reader for large files or network streams to avoid loading everything into memory. Use from_reader when memory usage is a constraint.

Use #[serde(rename_all = "camelCase")] when the API uses different casing than Rust conventions. Use rename attributes to bridge the gap between external data formats and internal code style.

Use Option<T> for fields that may be missing. Use Option to represent absence explicitly. Use #[serde(default)] only when a default value makes semantic sense.

Where to go next