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 = ©(&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:
serdeprovides theDeserializetrait used to convert data into Rust types.serde_jsonreads 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
Valueputs 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.
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);
fs::read_to_string("text.json")?opens the file and reads every character intocontents.contentscontains raw JSON text, including braces, quotes, and numbers.serde_json::from_str(&contents)?examines the JSON object.- Serde matches
"Age": 43to theage: u8field becauserename_all = "PascalCase"mapsagetoAge. - The number
43is converted into a Rustu8and stored inperson.age. println!prints43.
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::Valuewhen event fields vary by source.
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 = ©(&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
| Approach | Best when | Accessing Age | Main trade-off |
|---|---|---|---|
| Typed struct | The JSON format is known | person.age | You must define matching structs |
serde_json::Value | The shape is unknown or only partly needed | data["Age"].as_u64() | Type checks happen at runtime |
HashMap<String, Value> | Object keys are arbitrary | map.get("Age") | No fixed field names or nested structure validation |
| Reading method | Result | Suitable for JSON parsing? |
|---|---|---|
fs::read_to_string(path) | Complete file contents as String | Yes |
File::read_to_string(&mut String) | Appends file contents to a String | Yes |
io::copy(reader, writer) | Number of bytes copied | No; it transfers bytes to another writer |
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
structorHashMap - JSON array →
Vec<T> - JSON string →
String - JSON number →
u8,i64,f64, or another appropriate numeric type - JSON boolean →
bool - JSON
nullor 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
?overunwrap()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.
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
serdewith thederivefeature andserde_jsonto the project dependencies. - Create Rust structs for the person and nested address data.
- Read
text.jsonfrom 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
maininstead of usingunwrap().
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.