Serde and Configuration Patterns
Use this recipe for JSON APIs, configuration structs, environment loading, and secret-safe logging.
Configuration Type
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppConfig {
pub endpoint: String,
#[serde(default = "default_polling_interval_secs")]
pub polling_interval_secs: u64,
#[serde(default = "default_timeout_ms")]
pub timeout_ms: u64,
#[serde(default)]
pub telemetry: TelemetryConfig, // define TelemetryConfig in this file with the same derive + Default pattern
}
fn default_polling_interval_secs() -> u64 {
10
}
fn default_timeout_ms() -> u64 {
5_000
}
impl Default for AppConfig {
fn default() -> Self {
Self {
endpoint: String::new(),
polling_interval_secs: default_polling_interval_secs(),
timeout_ms: default_timeout_ms(),
telemetry: TelemetryConfig::default(),
}
}
}Redacted Secrets
#[derive(Clone, serde::Deserialize)]
pub struct SecretString(String);
impl SecretString {
pub fn expose_secret(&self) -> &str {
&self.0
}
}
impl std::fmt::Debug for SecretString {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("SecretString([REDACTED])")
}
}Use a secret-handling crate only when the project needs stronger guarantees such as zeroization.
Environment Loading
Avoid mutating environment variables at runtime. Load configuration once at startup:
impl AppConfig {
pub fn from_env() -> Result<Self> {
let endpoint = std::env::var("APP_ENDPOINT")
.map_err(|_| ServiceError::invalid_input("APP_ENDPOINT is required"))?;
let timeout_ms = std::env::var("APP_TIMEOUT_MS")
.ok()
.map(|value| value.parse::<u64>())
.transpose()
.map_err(|error| ServiceError::invalid_input(format!("APP_TIMEOUT_MS is invalid: {error}")))?
.unwrap_or_else(default_timeout_ms);
Ok(Self {
endpoint,
timeout_ms,
..Self::default()
})
}
}JSON API DTOs
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateUserRequest {
pub display_name: String,
pub email_address: String,
}Rules:
- Use
rename_allfor external JSON shape. - Keep API DTOs separate from domain types when validation or invariants differ.
- Use
#[serde(default)]for backward-compatible config evolution. - Avoid serializing secrets; if serialization is required, make it explicit.