Loading pageā¦
Rust walkthroughs
Loading pageā¦
#[serde(rename_all = "snake_case")] on field names during serialization vs deserialization?#[serde(rename_all = "snake_case")] applies a bidirectional transformation to all field names in a struct: during serialization, Rust's camelCase or PascalCase field names are converted to snake_case in the output; during deserialization, the attribute accepts either the renamed snake_case form or the original Rust field name. This creates a convention where your Rust code uses idiomatic naming while external formats (JSON, YAML, etc.) follow snake_case conventions. The transformation applies uniformly to all fields unless overridden by individual #[serde(rename = "...")] attributes, and it affects both directions of the serde pipelineāmaking your types accept snake_case input and produce snake_case output.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct UserConfig {
userName: String,
emailAddress: String,
isActive: bool,
}
fn main() {
let config = UserConfig {
userName: "alice".to_string(),
emailAddress: "alice@example.com".to_string(),
isActive: true,
};
// Serialization produces snake_case
let json = serde_json::to_string(&config).unwrap();
// {"user_name":"alice","email_address":"alice@example.com","is_active":true}
println!("{}", json);
}rename_all = "snake_case" converts userName to user_name in output.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
struct Config {
maxRetries: u32,
timeoutSeconds: u64,
}
fn main() {
// Accepts snake_case from JSON
let json = r#"{"max_retries": 3, "timeout_seconds": 30}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("{:?}", config);
// Also accepts the original Rust field name
let json2 = r#"{"maxRetries": 3, "timeoutSeconds": 30}"#;
let config2: Config = serde_json::from_str(json2).unwrap();
println!("{:?}", config2);
}Deserialization accepts both snake_case and original field names.
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
struct ApiResponse {
statusCode: u16,
errorMessage: String,
retryAfter: Option<u32>,
}
fn main() {
let response = ApiResponse {
statusCode: 404,
errorMessage: "Not Found".to_string(),
retryAfter: Some(60),
};
let json = serde_json::to_string_pretty(&response).unwrap();
println!("{}", json);
// Output:
// {
// "status_code": 404,
// "error_message": "Not Found",
// "retry_after": 60
// }
}Serialization always uses the transformed snake_case names.
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
struct SnakeCase {
myFieldName: String, // "my_field_name"
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CamelCase {
my_field_name: String, // "myFieldName"
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
struct PascalCase {
my_field_name: String, // "MyFieldName"
}
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
struct ScreamingSnake {
myFieldName: String, // "MY_FIELD_NAME"
}
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
struct KebabCase {
myFieldName: String, // "my-field-name"
}
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
struct Lowercase {
myFieldName: String, // "myfieldname"
}
#[derive(Serialize)]
#[serde(rename_all = "UPPERCASE")]
struct Uppercase {
myFieldName: String, // "MYFIELDNAME"
}Multiple case conversions are available for different naming conventions.
use serde::{Deserialize, Serialize};
// Rust convention: camelCase for struct fields
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct DatabaseRecord {
primaryKey: u64, // serializes to "primary_key"
createdAt: String, // serializes to "created_at"
updatedAt: String, // serializes to "updated_at"
isDeleted: bool, // serializes to "is_deleted"
}
// JSON convention: snake_case for API responses
// This struct bridges Rust style and JSON style
fn main() {
let record = DatabaseRecord {
primaryKey: 1,
createdAt: "2024-01-01".to_string(),
updatedAt: "2024-01-02".to_string(),
isDeleted: false,
};
let json = serde_json::to_string(&record).unwrap();
// {"primary_key":1,"created_at":"2024-01-01","updated_at":"2024-01-02","is_deleted":false}
}rename_all lets Rust code follow Rust conventions while JSON follows API conventions.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Config {
maxRetries: u32,
// Override the rename for this specific field
#[serde(rename = "API_KEY")]
apiKey: String,
timeoutMs: u32,
}
fn main() {
let config = Config {
maxRetries: 3,
apiKey: "secret".to_string(),
timeoutMs: 5000,
};
let json = serde_json::to_string(&config).unwrap();
// {"max_retries":3,"API_KEY":"secret","timeout_ms":5000}
println!("{}", json);
}Individual #[serde(rename = "...")] overrides rename_all for specific fields.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
struct Bidirectional {
firstName: String,
lastName: String,
}
fn main() {
// Serialization: Rust name ā snake_case
let person = Bidirectional {
firstName: "Alice".to_string(),
lastName: "Smith".to_string(),
};
let json = serde_json::to_string(&person).unwrap();
println!("Serialized: {}", json);
// {"first_name":"Alice","last_name":"Smith"}
// Deserialization: snake_case ā Rust name
let json_input = r#"{"first_name":"Bob","last_name":"Jones"}"#;
let person2: Bidirectional = serde_json::from_str(json_input).unwrap();
println!("Deserialized: {:?}", person2);
// Deserialization also accepts original names
let json_original = r#"{"firstName":"Carol","lastName":"White"}"#;
let person3: Bidirectional = serde_json::from_str(json_original).unwrap();
println!("Deserialized: {:?}", person3);
}The transformation applies symmetrically in both directions.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Status {
InProgress, // "in_progress"
Completed, // "completed"
Failed, // "failed"
NotStarted, // "not_started"
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Task {
id: u32,
status: Status,
errorMessage: String,
}
fn main() {
let task = Task {
id: 1,
status: Status::InProgress,
errorMessage: "None".to_string(),
};
let json = serde_json::to_string(&task).unwrap();
// {"id":1,"status":"in_progress","error_message":"None"}
println!("{}", json);
}rename_all applies to enum variants as well as struct fields.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Address {
streetName: String,
zipCode: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct User {
userName: String,
homeAddress: Address, // rename_all applies recursively
}
fn main() {
let user = User {
userName: "Bob".to_string(),
homeAddress: Address {
streetName: "Main St".to_string(),
zipCode: "12345".to_string(),
},
};
let json = serde_json::to_string(&user).unwrap();
// {"user_name":"Bob","home_address":{"street_name":"Main St","zip_code":"12345"}}
println!("{}", json);
}Each struct's rename_all applies independently to its own fields.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct BaseConfig {
maxConnections: u32,
timeoutSeconds: u64,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct FullConfig {
#[serde(flatten)]
base: BaseConfig,
enableDebug: bool,
}
fn main() {
let config = FullConfig {
base: BaseConfig {
maxConnections: 100,
timeoutSeconds: 30,
},
enableDebug: true,
};
let json = serde_json::to_string(&config).unwrap();
// {"max_connections":100,"timeout_seconds":30,"enable_debug":true}
// Flattened fields use their original rename_all transformation
}Flattened structs maintain their own rename_all transformation.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
struct Flexible {
userId: u32,
userName: String,
}
fn main() {
// Accepts snake_case (primary target)
let json1 = r#"{"user_id":1,"user_name":"Alice"}"#;
let f1: Flexible = serde_json::from_str(json1).unwrap();
// Also accepts original Rust names
let json2 = r#"{"userId":1,"userName":"Alice"}"#;
let f2: Flexible = serde_json::from_str(json2).unwrap();
// Both work during deserialization
println!("{:?}{:?}", f1, f2);
}Deserialization is flexible, accepting both forms.
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
struct Strict {
myValue: String,
}
fn main() {
let s = Strict {
myValue: "test".to_string(),
};
let json = serde_json::to_string(&s).unwrap();
// Always outputs "my_value", not "myValue"
assert_eq!(json, r#"{"my_value":"test"}"#);
}Serialization always uses the transformed name.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Complex {
XMLParser: String, // "xm_lparser" (unexpected splitting)
HTTPClient: String, // "htt_pclient" (unexpected splitting)
iOSVersion: String, // "i_os_version" (unexpected splitting)
}
fn main() {
let c = Complex {
XMLParser: "v1".to_string(),
HTTPClient: "v2".to_string(),
iOSVersion: "v3".to_string(),
};
let json = serde_json::to_string(&c).unwrap();
println!("{}", json);
// Note: serde's snake_case may not handle acronyms as expected
// Consider explicit rename for these cases
}Acronyms and special patterns may need manual rename attributes.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Better {
#[serde(rename = "xml_parser")]
XMLParser: String,
#[serde(rename = "http_client")]
HTTPClient: String,
#[serde(rename = "ios_version")]
iOSVersion: String,
}
fn main() {
let b = Better {
XMLParser: "v1".to_string(),
HTTPClient: "v2".to_string(),
iOSVersion: "v3".to_string(),
};
let json = serde_json::to_string(&b).unwrap();
// {"xml_parser":"v1","http_client":"v2","ios_version":"v3"}
println!("{}", json);
}Use explicit rename for fine-grained control over specific fields.
use serde::{Deserialize, Serialize};
// Container-level rename_all applies to all fields
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Container {
myField: String, // "my_field"
}
// Field-level rename applies to that field only
#[derive(Serialize, Deserialize)]
struct FieldLevel {
#[serde(rename = "my_field")]
myField: String, // "my_field"
}
// Container-level is more concise for multiple fieldsContainer-level rename_all is more concise when all fields need transformation.
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
struct WithSkip {
includeThis: String,
#[serde(skip_serializing)]
internalField: String,
alsoIncluded: u32,
}
fn main() {
let s = WithSkip {
includeThis: "yes".to_string(),
internalField: "no".to_string(),
alsoIncluded: 42,
};
let json = serde_json::to_string(&s).unwrap();
// {"include_this":"yes","also_included":42}
println!("{}", json);
// Skipped fields don't appear in output
}Skipped fields are excluded from serialization entirely.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
struct RoundTrip {
fieldName: String,
anotherField: u32,
}
fn main() {
let original = RoundTrip {
fieldName: "test".to_string(),
anotherField: 42,
};
// Serialize to snake_case
let json = serde_json::to_string(&original).unwrap();
// Deserialize from snake_case
let restored: RoundTrip = serde_json::from_str(&json).unwrap();
assert_eq!(original.fieldName, restored.fieldName);
assert_eq!(original.anotherField, restored.anotherField);
// Round-trip preserves data correctly
}Serialization and deserialization are consistent for round-trips.
| Attribute | Serialization | Deserialization |
|-----------|--------------|-----------------|
| rename_all = "snake_case" | Output uses snake_case | Accepts snake_case and original |
| rename = "specific_name" | Output uses specific_name | Accepts specific_name and original |
| No attribute | Output uses Rust name | Accepts Rust name only |
#[serde(rename_all = "snake_case")] creates a bidirectional mapping between Rust naming conventions and external format conventions:
Serialization behavior: Rust field names are transformed to snake_case. A field named userName becomes "user_name" in JSON. This transformation is strictāserialized output always uses the transformed name.
Deserialization behavior: The attribute makes deserialization more flexible. It accepts both the transformed snake_case name ("user_name") and the original Rust field name ("userName"). This flexibility allows gradual migration and compatibility with multiple input formats.
Key implications:
camelCase while APIs use snake_caserename_allrenamerename for fine control)rename_all attributeBest practice: Use rename_all at the container level when all fields should follow the same convention. Use field-level rename to override specific fields or handle edge cases like acronyms. Choose the case convention that matches your external API requirementsāsnake_case for JSON APIs following that convention, camelCase for JavaScript interop, etc.