Rust新手求教:如何为Serde实现自定义rename_all属性?
Q1: What is #[serde(rename_all = "...")], is it a macro rule, and where is it implemented in Serde's source?
#[serde(rename_all = "...")] is a container-level attribute provided by Serde to handle bulk renaming of struct/enum fields during serialization/deserialization. For example, rename_all = "snake_case" tells Serde to automatically map struct fields in camelCase to snake_case in JSON (or vice versa for deserialization).
It's not a standalone macro rule—it's an attribute annotation recognized by Serde's derive procedural macros (specifically #[derive(Serialize, Deserialize)]). When you use these derive macros, Serde parses the #[serde(...)] attributes at compile time and generates the corresponding serialization/deserialization code tailored to your renaming rules.
As for where to find the implementation in Serde's source code:
- Attribute parsing logic: Look in the
serde_derivecrate'ssrc/attr.rsfile. This is where Serde defines how to interpret parameters likerename_allfrom the#[serde]attribute. - Core naming conversion logic: The
Caseenum (which includes built-in styles likeSnakeCase,CamelCase) and its associated conversion functions live inserdecrate'ssrc/rename.rs. This is where the actual string transformation forrename_allhappens. - Code generation for derive macros: The
serde_derivecrate'ssrc/internalsdirectory (e.g.,ast.rsfor abstract syntax tree definitions,codegen.rsfor generating the final ser/de code) uses the parsed attributes to generate the right code for your structs/enums.
Q2: How to implement a custom container attribute to deserialize Title Case JSON to snake_case structs?
Serde doesn't support Title Case (words capitalized, separated by spaces) out of the box for rename_all, but there are a few practical ways to solve this:
Option 1: Use the serde_with crate (recommended for simplicity)
serde_with is a popular Serde extension that adds support for custom naming transformations. Here's how to use it:
First, add the dependencies to your Cargo.toml:
serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.0"
Then define your conversion function and apply the attribute:
use serde::Deserialize; use serde_with::RenameAllWith; // Convert Title Case strings to snake_case fn title_case_to_snake_case(input: &str) -> String { input .to_lowercase() .replace(' ', "_") .trim_matches('_') .replace("__", "_") // Handle multiple spaces if present } #[derive(Deserialize, Debug)] #[serde_with::serde_as] #[serde_with::rename_all_with(title_case_to_snake_case)] struct Vehicle { car_prop: i32, door_prop: i32, } fn main() -> Result<(), serde_json::Error> { let json = r#"{"Car Prop": 1, "Door Prop": 2}"#; let vehicle: Vehicle = serde_json::from_str(json)?; println!("{:?}", vehicle); // Output: Vehicle { car_prop: 1, door_prop: 2 } Ok(()) }
Option 2: Manually implement the Deserialize trait (no extra dependencies)
If you don't want to add a third-party crate, you can hand-write the Deserialize implementation to map Title Case keys to your snake_case fields:
use serde::Deserialize; use serde_json::Value; #[derive(Debug)] struct Vehicle { car_prop: i32, door_prop: i32, } // Helper to convert Title Case to snake_case fn title_to_snake(s: &str) -> String { s.split_whitespace() .map(|word| word.to_lowercase()) .collect::<Vec<_>>() .join("_") } impl<'de> Deserialize<'de> for Vehicle { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { let raw_obj = Value::deserialize(deserializer)?; let obj = raw_obj.as_object() .ok_or_else(|| serde::de::Error::custom("expected a JSON object"))?; // Map each Title Case JSON key to the corresponding struct field let car_prop = obj.get("Car Prop") .ok_or_else(|| serde::de::Error::missing_field("Car Prop"))? .deserialize()?; let door_prop = obj.get("Door Prop") .ok_or_else(|| serde::de::Error::missing_field("Door Prop"))? .deserialize()?; Ok(Vehicle { car_prop, door_prop }) } } fn main() -> Result<(), serde_json::Error> { let json = r#"{"Car Prop": 1, "Door Prop": 2}"#; let vehicle: Vehicle = serde_json::from_str(json)?; println!("{:?}", vehicle); Ok(()) }
Option 3: Write a custom derive macro (for large-scale use)
If you have many structs needing this conversion, you can build a custom procedural macro to automate the process. This requires learning Rust's proc-macro system, but the basic steps are:
- Create a new proc-macro crate.
- Parse the struct's abstract syntax tree (AST) to extract field names.
- Generate
Deserializeimplementation code that maps each snake_case field to its Title Case JSON key. - Use your custom
#[derive(TitleCaseDeserialize)]attribute in your main code.
This is more complex but scales well for multiple structs.
内容的提问来源于stack exchange,提问作者Nona

