Introduction: Why JSON is Important for Rust Developers
JSON (JavaScript Object Notation) remains one of the most popular data exchange formats in web development, microservices, and APIs. For the Rust language, which is rapidly gaining popularity due to its performance and safety, working with JSON is a critical task. Whether you are a backend developer using Actix or Rocket, writing a CLI utility, or working with web sockets — the ability to parse and serialize JSON in Rust will definitely come in handy.
In this article, we will break down how to efficiently work with JSON in Rust using the standard ecosystem of crates serde and serde_json. You will learn how to convert data structures to and from JSON, handle errors, and work with dynamic data.
Basics: Installing and Adding Dependencies
To work with JSON in Rust, the powerful serialization framework Serde and its JSON implementation — the serde_json crate — are used. To get started, add them to your Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
The features = ["derive"] flag allows using the #[derive(Serialize, Deserialize)] macros, which significantly simplifies the code.
Basic JSON String Parsing
The simplest way to start is to parse a JSON string into the universal type serde_json::Value. This is analogous to serde_json::Value in Python or object in JavaScript:
use serde_json::Value;
fn main() {
let data = r#"
{
"name": "Rust",
"year": 2015,
"features": ["safe", "fast", "concurrent"]
}"#;
let parsed: Value = serde_json::from_str(data).expect("JSON parsing error");
println!("Name: {}", parsed["name"]);
println!("Year: {}", parsed["year"]);
println!("First feature: {}", parsed["features"][0]);
}
The Value type can be: Null, Bool, Number, String, Array, or Object. This is convenient for prototyping, but for production code it is better to use strict typing.
Serialization and Deserialization of Structs
The main strength of Serde is the automatic conversion between Rust structs and JSON. Let's look at an example:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
id: u32,
name: String,
email: String,
active: bool,
}
fn main() {
// Deserialization (JSON -> Rust)
let json_str = r#"{"id":1,"name":"Alexey","email":"alex@example.com","active":true}"#;
let user: User = serde_json::from_str(json_str).unwrap();
println!("User: {:?}", user);
// Serialization (Rust -> JSON)
let new_user = User {
id: 2,
name: String::from("Maria"),
email: String::from("maria@example.com"),
active: false,
};
let json_output = serde_json::to_string_pretty(&new_user).unwrap();
println!("JSON:{}", json_output);
}
Note to_string_pretty — it formats JSON with indentation for readability. For production, use to_string (without extra whitespace).
Working with Nested Structures
JSON often contains nested objects and arrays. Serde handles this easily:
#[derive(Serialize, Deserialize)]
struct Project {
name: String,
languages: Vec<String>,
contributors: Vec<Contributor>,
}
#[derive(Serialize, Deserialize)]
struct Contributor {
username: String,
contributions: u32,
}
fn main() {
let project_json = r#"
{
"name": "Serde",
"languages": ["Rust", "Python", "C++"],
"contributors": [
```