Rust 12. Serde with JSON and TOML Basics
Summary
Real Rust programs do not stop at strings, numbers, and collections. At some point they need to read configuration files, API responses, logs, or other external data formats, which means the real question becomes how to move between Rust types and serialized data safely.
This post introduces the most basic pattern for JSON and TOML using serde, serde_json, and toml. The practical beginner flow is to define a struct as the data boundary, derive Serialize and Deserialize, and then use crate APIs such as from_str and to_string_pretty for each format.
Document Information
- Written on: 2026-04-15
- Verification date: 2026-04-16
- Document type: tutorial
- Test environment: Windows 11 Pro, Windows PowerShell, Cargo CLI examples
- Test version: rustc 1.94.0, cargo 1.94.0, serde 1.0.228, serde_json 1.0.149, toml 1.1.2+spec-1.1.0
- Source grade: official documentation and official crate documentation are used.
- Note: this post stays with the basic typed struct workflow for serialization and deserialization and does not cover custom attributes or advanced enum tagging strategies.
Problem Definition
After file I/O and CLI input, the next natural step is external data formats. Beginners usually get stuck at a few common points.
- It is not obvious whether JSON should be handled as a generic map or as a typed struct.
- The roles of
serde,serde_json, andtomlcan feel blurry at first. - The direction of
SerializeversusDeserializeis easy to mix up in the beginning.
This post stays with the most basic typed-data conversion flow. It does not cover custom serializers, high-performance streaming parsers, or more advanced enum-tagging strategies.
Verified Facts
- According to the official documentation, Serde is a framework for serializing and deserializing Rust data structures, with
SerializeandDeserializeas its central traits. Evidence: Serde crate docs, Serde overview - According to the official docs, enabling the
derivefeature allows automatic generation ofSerializeandDeserializeimplementations for structs and enums. Evidence: Using derive - According to the official docs,
serde_json::from_strandserde_json::to_string_prettyare used to convert between JSON strings and Rust values. Evidence: serde_json crate docs - According to the official docs,
toml::from_strandtoml::to_string_prettyare used to convert between TOML strings and Rust values. Evidence: toml crate docs
As of April 15, 2026, the latest pages on docs.rs supported using dependency lines like:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "1"
A very common typed boundary looks like this:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
app_name: String,
port: u16,
debug: bool,
}
Reading JSON into that type can look like this:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let json = r#"{
"app_name": "rust-tool",
"port": 8080,
"debug": true
}"#;
let config: AppConfig = serde_json::from_str(json)?;
println!("JSON => {:?}", config);
Ok(())
}
The same struct can be reused for TOML.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let text = r#"
app_name = "rust-tool"
port = 8080
debug = true
"#;
let config: AppConfig = toml::from_str(text)?;
let pretty = toml::to_string_pretty(&config)?;
println!("TOML => {:?}", config);
println!("{}", pretty);
Ok(())
}
The important design point is that even when the external format changes, the application can keep using the same internal Rust type. That makes the struct the stable boundary and the file format the replaceable layer.
Directly Confirmed Results
- Directly confirmed result: the Rust toolchain versions available in the current writing environment were:
rustc --version
cargo --version
- Observed output:
rustc 1.94.0 (4a4ef493e 2026-03-02)
cargo 1.94.0 (85eff7c80 2026-01-15)
- Directly confirmed result: when I combined the JSON and TOML examples into one
mainfunction in a temporary Cargo project withserde,serde_json, andtomladded, the output was:
cargo run --quiet
- Observed output:
JSON => AppConfig { app_name: "rust-tool", port: 8080, debug: true }
TOML => AppConfig { app_name: "rust-tool", port: 8080, debug: true }
app_name = "rust-tool"
port = 8080
debug = true
- Limitation of direct reproduction: I reproduced the basic JSON and TOML conversion flow in a temporary Cargo project, but I did not validate extended cases such as nested types, enum tagging, or custom field attributes.
Interpretation / Opinion
- My view is that the most important beginner Serde habit is to define a type boundary first instead of manually manipulating raw JSON strings.
- Opinion: for small config files and API responses, deserializing directly into a struct is usually easier to validate, test, and maintain than starting from a generic
Valueor map. - Opinion: format-specific parsing crates may change over time, but internal application types tend to live longer, so it is worth stabilizing the struct design early.
Limits and Exceptions
- This post covers only basic struct conversion and leaves out nested enums, custom field attributes, flattening, renaming rules, and borrowed deserialization.
- Large JSON streams or very strict validation requirements may need different tools and patterns.
- TOML and JSON have different representation rules, so the same struct will not always map equally naturally in every case.
- Dependency versions can change over time, so in a real project it is safer to check the post’s verification date together with the latest crate pages.
댓글남기기