TL;DR: Serde (Serialize/Deserialize) is Rust's universal serialization framework; the most downloaded crate in the Rust ecosystem. Add
#[derive(Serialize, Deserialize)]to any struct or enum, and Serde can convert it to and from JSON, TOML, YAML, MessagePack, Bincode, and 20+ other formats. Serde generates the conversion code at compile time via procedural macros; zero runtime reflection, zero overhead compared to hand-written code.
What Is Serde?
Serde is a framework for serializing Rust data structures to and from many formats; it separates the data model (your types) from the format (JSON, TOML, etc.) via a common interface.
# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
email: String,
active: bool,
}
fn main() {
let user = User {
name: "Alice".to_string(),
age: 30,
email: "[email protected]".to_string(),
active: true,
};
// Serialize to JSON string
let json = serde_json::to_string(&user).unwrap();
println!("{json}");
// {"name":"Alice","age":30,"email":"[email protected]","active":true}
// Deserialize from JSON string
let json_str = r#"{"name":"Bob","age":25,"email":"[email protected]","active":false}"#;
let bob: User = serde_json::from_str(json_str).unwrap();
println!("{:?}", bob);
}How Do You Customize Serde Behavior?
Serde provides field-level and container-level attributes for renaming, skipping, flattening, and transforming fields during serialization.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] // snake_case fields → camelCase in JSON
struct ApiResponse {
user_id: u64, // serialized as "userId"
created_at: String, // serialized as "createdAt"
#[serde(skip_serializing_if = "Option::is_none")]
optional_field: Option<String>, // omitted from JSON when None
#[serde(rename = "type")]
kind: String, // serialized as "type" (reserved keyword workaround)
#[serde(skip)]
internal_cache: String, // never serialized or deserialized
#[serde(default)]
score: u32, // defaults to 0 if missing from input
}Common attributes:
| Attribute | What it does |
|---|---|
#[serde(rename = "name")] | Use different name in output |
#[serde(rename_all = "camelCase")] | Transform all field names |
#[serde(skip)] | Exclude field entirely |
#[serde(skip_serializing_if = "...")] | Conditional skip |
#[serde(default)] | Use Default::default() when field missing |
#[serde(flatten)] | Inline nested struct fields |
#[serde(tag = "type")] | Internally tagged enum variants |
How Does Serde Work With APIs and Web Frameworks?
Axum, Actix-web, and Rocket all use Serde for request/response bodies; Json<T> automatically deserializes request JSON and serializes response structs.
use axum::{Json, Router, routing::post};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct UserCreated {
id: u64,
name: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<UserCreated> {
// payload is already deserialized; Axum used Serde automatically
Json(UserCreated {
id: 42,
name: payload.name,
})
// Response is automatically serialized to JSON
}What Formats Does Serde Support?
The serde crate defines the framework; format-specific crates implement the actual encoding.
| Format | Crate | Use case |
|---|---|---|
| JSON | serde_json | APIs, config files |
| TOML | toml | Cargo.toml, config files |
| YAML | serde_yaml | Config files, Kubernetes manifests |
| MessagePack | rmp-serde | Compact binary, fast |
| Bincode | bincode | Internal Rust-to-Rust binary |
| CBOR | ciborium | IoT, compact binary |
| CSV | csv | Tabular data |
| URL-encoded | serde_urlencoded | HTML form data |
Switch formats by changing one import; your #[derive(Serialize, Deserialize)] types work with all of them.
Frequently Asked Questions
Serde's #[derive(Serialize, Deserialize)] generates specialized code for each type at compile time; no reflection, no runtime type inspection. The generated code is as fast as hand-written serialization.
Yes; implement the Serializer/Deserializer visitor pattern directly. This is needed for types with complex invariants or when deriving is not possible. It is verbose but powerful. See the Serde guide for examples.
#[serde(deny_unknown_fields)] rejects JSON with extra fields (strict parsing). Without it, unknown fields are silently ignored by default. For capturing unknown fields, use #[serde(flatten)] with a HashMap<String, serde_json::Value>.
serde_json::Value is a dynamic JSON value; an enum representing any JSON type (null, bool, number, string, array, object). Use it when you don't know the schema at compile time or need to work with arbitrary JSON.
Sources
- Serde Documentation: Official guide with all attributes
- serde on crates.io: The most downloaded Rust crate
Related Glossary Terms
- Struct: Structs are the primary Serde target via
#[derive] - Enum: Serde supports all enum shapes with
#[serde(tag)] - Macro:
#[derive(Serialize, Deserialize)]are procedural macros - Axum: Axum uses Serde for
Json<T>extractors and responses - serde_json: serde_json is the default JSON format most Serde users start with
- Chrono: Serde is commonly used to serialize and deserialize Rust date-time types
