Question

Parse JSON Files in Rust with serde_json

rustjson

Question

I want to parse JSON data from a file in Rust and retrieve values for keys such as Age.

I currently have this code:

extern crate rustc_serialize;

use rustc_serialize::json::Json;
use std::fs::File;
use std::io::copy;
use std::io::stdout;

fn main() {
    let mut file = File::open("text.json").unwrap();
    let mut stdout = stdout();
    let mut str = &copy(&mut file, &mut stdout).unwrap().to_string();
    let data = Json::from_str(str).unwrap();
}

The text.json file contains an object with fields including FirstName, LastName, Age, a nested Address object, and a PhoneNumbers array. What is the correct next step to read the file as JSON and access a value such as Age?

Short Answer

You will learn the two main steps of JSON parsing in Rust: read the file into a String, then deserialize that text into Rust data. You will see both dynamic access for unknown JSON shapes and typed structs for reliable application code.

Concept

JSON is text, while Rust programs work with values such as strings, numbers, structs, and vectors. Parsing JSON means converting JSON text into one of those usable Rust representations.

The modern Rust ecosystem uses two crates together:

  • serde provides the Deserialize trait used to convert data into Rust types.
  • serde_json reads and writes JSON.

For a known file format, define Rust structs that match the JSON structure and deserialize directly into them. This is usually the best approach because Rust verifies that required fields and expected types are present.

For an unknown or changing JSON format, deserialize into serde_json::Value. A Value is a dynamic JSON tree that can represent an object, array, string, number, boolean, or null.

The older rustc_serialize crate shown in the question is obsolete for new Rust code. Use serde and serde_json instead.

Also, std::io::copy is not a JSON-reading function: it copies bytes from one stream to another and returns the number of bytes copied. To parse a file, use std::fs::read_to_string or File::read_to_string to obtain the actual file contents.

Mental Model

Think of a JSON file as a form written in a common text format.

  • Reading the file brings the paper into your program as a String.
  • Deserializing into a struct fills out a Rust-specific form with named, typed fields.
  • Deserializing into Value puts the JSON into a flexible box where you inspect labels such as "Age" at runtime.

A typed struct is like requesting a specific form: age must be a number and address must have the expected fields. A Value is like receiving an unstructured document and looking through it yourself.

Take Quiz

Syntax and Examples

Add these dependencies to Cargo.toml:

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

Recommended: deserialize into structs

use serde::Deserialize;
use std::fs;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Person {
    first_name: String,
    last_name: String,
    age: u8,
    address: Address,
    phone_numbers: Vec<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Address {
    street: String,
    city: String,
    country: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let contents = fs::read_to_string("text.json")?;
    let person: Person = serde_json::from_str(&contents)?;

    println!("{} is {} years old.", person.first_name, person.age);
    println!("City: {}", person.address.city);

    Ok(())
}

fs::read_to_string returns the file's text. serde_json::from_str parses that text and creates a Person. The #[serde(rename_all = "PascalCase")] attribute lets idiomatic Rust fields such as first_name map to JSON keys such as "FirstName".

Dynamic access with Value

use serde_json::Value;
use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let contents = fs::read_to_string("text.json")?;
    let data: Value = serde_json::from_str(&contents)?;

    let age = data["Age"].as_u64();
    println!("Age: {age:?}");

    Ok(())
}

as_u64() returns Option<u64>, so it produces Some(43) for the example file. It produces None if Age is missing, null, negative, or not an unsigned integer.

Step by Step Execution

Consider this typed version:

let contents = fs::read_to_string("text.json")?;
let person: Person = serde_json::from_str(&contents)?;
println!("{}", person.age);
  1. fs::read_to_string("text.json")? opens the file and reads every character into contents.
  2. contents contains raw JSON text, including braces, quotes, and numbers.
  3. serde_json::from_str(&contents)? examines the JSON object.
  4. Serde matches "Age": 43 to the age: u8 field because rename_all = "PascalCase" maps age to Age.
  5. The number 43 is converted into a Rust u8 and stored in person.age.
  6. println! prints 43.

If the file contained "Age": "forty-three", deserialization would return an error because that value is a JSON string, not a number.

Real World Use Cases

  • Application configuration: Read a JSON settings file containing server ports, feature flags, or API endpoints.
  • API responses: Deserialize JSON returned by a weather, payment, or user-profile API into structs.
  • Import tools: Load customer records, product catalogs, or report data from JSON exports.
  • Command-line programs: Read structured input and select values such as a user's name, ID, or permissions.
  • Logging and events: Inspect loosely structured event JSON with serde_json::Value when event fields vary by source.
Take Quiz

Real Codebase Usage

In production Rust code, developers usually prefer typed structs at boundaries: when reading a file, receiving an HTTP response, or loading configuration. This keeps the rest of the program type-safe.

Use error propagation rather than unwrap() when input can fail:

fn load_person(path: &str) -> Result<Person, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string(path)?;
    let person = serde_json::from_str(&contents)?;
    Ok(person)
}

Use optional fields when JSON input may omit them:

#[derive(serde::Deserialize)]
struct Person {
    #[serde(rename = "FirstName")]
    first_name: String,
    #[serde(rename = "Age")]
    age: Option<u8>,
}

Option<u8> represents either a present numeric age (Some(43)) or a missing/null age (None). For JSON objects with arbitrary keys, use a map such as std::collections::HashMap<String, serde_json::Value> instead of creating fields that are not known in advance.

Common Mistakes

Using copy to obtain JSON text

This does not read the file into a string:

let mut text = &copy(&mut file, &mut stdout()).unwrap().to_string();

copy writes the file bytes to standard output and returns a byte count, such as 214. Parsing that count as JSON is not parsing the file. Use:

let text = std::fs::read_to_string("text.json")?;

Treating a JSON number as a string

This JSON value is a number:

"Age": 43

Therefore the Rust field should be numeric:

age: u8

Not:

age: String

Using the wrong key capitalization with Value

JSON keys are case-sensitive:

let age = data["age"].as_u64(); // None for a file containing "Age"

Use data["Age"], or map the key with #[serde(rename = "Age")] when using a struct.

Calling unwrap() on external input

unwrap() terminates the program if the file is missing or the JSON is invalid. It can be acceptable for a quick experiment, but use ?, match, or if let for files users or systems can change.

Assuming every Value lookup succeeds

Avoid immediately unwrapping dynamic values:

let age = data["Age"].as_u64().unwrap();

Prefer handling the optional result:

if let Some(age) = data["Age"].as_u64() {
    println!("Age: {age}");
}

Comparisons

ApproachBest whenAccessing AgeMain trade-off
Typed structThe JSON format is knownperson.ageYou must define matching structs
serde_json::ValueThe shape is unknown or only partly neededdata["Age"].as_u64()Type checks happen at runtime
HashMap<String, Value>Object keys are arbitrarymap.get("Age")No fixed field names or nested structure validation
Reading methodResultSuitable for JSON parsing?
fs::read_to_string(path)Complete file contents as StringYes
File::read_to_string(&mut String)Appends file contents to a StringYes
io::copy(reader, writer)Number of bytes copiedNo; it transfers bytes to another writer
Take Quiz

Cheat Sheet

# Cargo.toml
serde = { version = "1", features = ["derive"] }
serde_json = "1"
// Read a file as text
let text = std::fs::read_to_string("text.json")?;

// Parse known JSON into a struct
let person: Person = serde_json::from_str(&text)?;
println!("{}", person.age);

// Parse flexible JSON
let value: serde_json::Value = serde_json::from_str(&text)?;
let age: Option<u64> = value["Age"].as_u64();
  • JSON object → Rust struct or HashMap
  • JSON array → Vec<T>
  • JSON string → String
  • JSON number → u8, i64, f64, or another appropriate numeric type
  • JSON boolean → bool
  • JSON null or missing optional field → Option<T>
  • JSON key names are case-sensitive.
  • Use #[serde(rename = "JsonKey")] for one field, or #[serde(rename_all = "PascalCase")] for a naming style.
  • Prefer ? over unwrap() for file and parse errors.

FAQ

How do I parse a JSON file in Rust?

Read it into a string with std::fs::read_to_string, then call serde_json::from_str to deserialize it into a struct or serde_json::Value.

How do I get the Age key from JSON in Rust?

With a struct, use person.age. With serde_json::Value, use data["Age"].as_u64() and handle the resulting Option<u64>.

Why does io::copy not work for parsing JSON?

io::copy transfers bytes to a destination such as standard output and returns the byte count. It does not return the file's contents.

Should I use serde_json::Value or a struct?

Use a struct when you know the JSON format. Use Value for exploratory code or data with a genuinely variable structure.

What does #[serde(rename_all = "PascalCase")] do?

It maps Rust field names such as first_name to JSON keys such as FirstName during serialization and deserialization.

What happens if a required JSON field is missing?

Deserialization into a normal struct field returns an error. Declare the field as Option<T> if missing or null is valid.

Is rustc_serialize still recommended for JSON in Rust?

No. New Rust projects should use serde and serde_json.

Take Quiz

Mini Project

Description

Build a small contact-file reader. It loads a person record from JSON, validates that the required fields match the expected types through deserialization, and prints a concise contact summary.

Goal

Read text.json and display the person's full name, age, city, and phone numbers.

Requirements

  • Add serde with the derive feature and serde_json to the project dependencies.
  • Create Rust structs for the person and nested address data.
  • Read text.json from the current working directory.
  • Deserialize the JSON into the person struct.
  • Print the full name, age, city, and each phone number.
  • Return parsing and file errors from main instead of using unwrap().
Take Quiz

Keep learning

Related questions

View more