RUST Contents

Serde Derive Basics

Use Serde derive macros to serialize and deserialize data safely, control field mapping, and design stable JSON contracts for production Rust services.

On this page

Serialization Is a Contract Boundary

In production systems, serialization is not just about converting structs to JSON. It defines your API contract with clients, databases, queues, and other services. Once exposed, that contract becomes hard to change. Serde is the de facto standard library for serialization in Rust and must be used deliberately.

Adding Serde to Your Project

# Cargo.toml
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

The derive feature allows automatic implementation of Serialize and Deserialize traits.

Basic Derive Example

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct User {
    id: u64,
    email: String,
}

fn main() {
    let user = User {
        id: 1,
        email: "a@example.com".to_string(),
    };

    let json = serde_json::to_string(&user).unwrap();
    println!("{}", json);
}

This automatically maps struct fields to JSON keys with the same names.

Deserializing JSON

let input = r#"{"id":1,"email":"a@example.com"}"#;

let user: User = serde_json::from_str(input).unwrap();
println!("{}", user.email);

Production note: never unwrap in real request handlers. Always return proper error responses.

Renaming Fields for API Stability

Field names in Rust do not always match external API conventions. Use attributes to control mapping.

#[derive(Serialize, Deserialize)]
struct User {
    #[serde(rename = "user_id")]
    id: u64,

    #[serde(rename = "email_address")]
    email: String,
}

This keeps internal naming clean while preserving external contract stability.

Optional Fields

Optional JSON fields map naturally to Option types.

#[derive(Serialize, Deserialize)]
struct CreateUser {
    email: String,
    nickname: Option,
}

If nickname is missing, it becomes None.

Skipping Fields

Sometimes you do not want to expose internal fields.

#[derive(Serialize, Deserialize)]
struct User {
    id: u64,
    email: String,

    #[serde(skip_serializing)]
    internal_flag: bool,
}

Production rule: never serialize secrets or internal flags unintentionally.

Default Values

When deserializing from older clients, fields may be missing. Use default values to preserve backward compatibility.

#[derive(Serialize, Deserialize)]
struct Config {
    #[serde(default)]
    retries: u32,
}

This prevents breaking changes when introducing new fields.

Strict vs Lenient Deserialization

By default, unknown fields are ignored. You can enforce stricter validation.

#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictUser {
    id: u64,
    email: String,
}

Production rule: use deny_unknown_fields when strict API validation is required.

Error Handling During Deserialization

Never unwrap JSON parsing in production. Convert errors into structured responses.

fn parse_user(input: &str) -> Result {
    serde_json::from_str(input)
        .map_err(|e| format!("invalid JSON payload: {}", e))
}

Attach context, but avoid logging raw request bodies if they may contain sensitive data.

Versioning Considerations

API contracts evolve. Backward compatibility strategies include:

  • Adding optional fields
  • Using default values
  • Avoiding renaming existing fields without versioning
  • Introducing new endpoint versions instead of breaking contracts

Serde makes it easy to maintain backward compatibility, but design discipline is still required.

Production Pitfalls

  • Unwrapping JSON parsing errors
  • Accidentally serializing secrets
  • Changing field names without versioning
  • Using overly permissive deserialization without validation

Production Checklist

  • All external structs derive Serialize and Deserialize intentionally
  • Field renaming explicit when API differs from internal naming
  • Optional fields use Option
  • Defaults applied for backward compatibility
  • No secrets serialized
  • Deserialization errors mapped to structured responses

Serde is more than a convenience tool. It defines the boundary between your Rust service and the outside world. Treat it as part of your production contract.