Serde (Rust): Custom Behavior & Supported Formats Guide

Max WellsMax WellsFounder of Rustify

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:

AttributeWhat 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.

FormatCrateUse case
JSONserde_jsonAPIs, config files
TOMLtomlCargo.toml, config files
YAMLserde_yamlConfig files, Kubernetes manifests
MessagePackrmp-serdeCompact binary, fast
BincodebincodeInternal Rust-to-Rust binary
CBORciboriumIoT, compact binary
CSVcsvTabular data
URL-encodedserde_urlencodedHTML 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


  • 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

Keep Reading

Ready to Land a $80-120k Rust Job?