Java's MapStruct Implemented in Rust
Daniel Hayes
Full-Stack Engineer · Leapcell

In the Java ecosystem, there is a bean conversion tool called MapStruct, which makes it very convenient to convert between beans. Its principle is to generate the conversion methods at compile time. Since Rust macros also support generating code at compile time, I decided to implement a simple version of MapStruct using attribute macros.
Macro Basics in Rust
Macros in Rust are divided into two main categories: declarative macros (macro_rules!
) and three types of procedural macros:
- Derive Macros: These are commonly used to derive specific code for target structs or enums, such as the
Debug
trait. - Attribute-like Macros: These are used to add custom attributes to targets.
- Function-like Macros: These look similar to function calls.
Analysis of Implementation Principles
If you want to convert between beans in Rust, it's quite straightforward—you can implement the From
trait and define the conversion logic inside the from
method.
pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } impl From<Person> for PersonDto { fn from(item: Person) -> PersonDto { PersonDto { name: item.name, age: item.age, } } } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // Use the auto-generated From implementation for conversion println!("dto: name:{}, age:{}", dto.name, dto.age); }
So, to implement this in Rust using macros, we need to have the macro automatically generate the From
method, thus enabling automatic conversion.
For ease of use, I took inspiration from Diesel’s syntax like #[diesel(table_name = blog_users)]
. Our macro can be used by simply adding #[auto_map(target = "PersonDto")]
above a struct—very clean and elegant.
#[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, }
Code Implementation
Since the macro usage is #[auto_map(target = "PersonDto")]
, the macro’s workflow is roughly fixed. Taking Person
and PersonDto
as examples, the process is as follows:
- Extract the
"target"
parameter from the macro. - Parse the input struct (
Person
). - Extract the field names and types from the input struct.
- Parse the target type.
- Regenerate the original struct and implement the
From
method.
Step 1: Create the Project and Add Dependencies
cargo new rust_mapstruct --lib cd rust_mapstruct
Since macro code generation requires parsing Rust’s AST, you need two key libraries: quote
and syn
. Also, since we're creating macros, you need to specify proc-macro = true
.
Complete dependencies:
[lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0.17", features = ["full"] }
Step 2: Modify lib.rs
Core Code
1. Define the Core Function
#[proc_macro_attribute] pub fn auto_map(args: TokenStream, input: TokenStream) -> TokenStream { }
2. Extract and Parse the "target" Parameter
This could be extended to support multiple parameters, but since our MapStruct-like tool only needs one, we directly match on the target
string. You can expand on this to add more parameters later.
let args = parse_macro_input!(args as AttributeArgs); // Extract and parse the "target" parameter let target_type = args .iter() .find_map(|arg| { if let NestedMeta::Meta(Meta::NameValue(m)) = arg { if m.path.is_ident("target") { if let Lit::Str(lit) = &m.lit { return Some(lit.value()); } } } None }) .expect("auto_map requires a 'target' argument");
3. Parse the Input Struct (Person
)
// Parse the input struct let input = parse_macro_input!(input as DeriveInput); let struct_name = input.ident; let struct_data = match input.data { Data::Struct(data) => data, _ => panic!("auto_map only supports structs"), };
4. Extract Field Names and Types from Person
let (field_names, field_mappings): (Vec<_>, Vec<_>) = struct_data.fields.iter().map(|f| { let field_name = f.ident.as_ref().unwrap(); let field_type = &f.ty; (field_name.clone(), quote! { #field_name: #field_type }) }).unzip();
5. Parse the Target Type (PersonDto
)
syn::parse_str
can convert a string into a Rust type.
// Parse the target type let target_type_tokens = syn::parse_str::<syn::Type>(&target_type).unwrap();
6. Generate the Original Struct and the From
Implementation
The code inside quote
acts as a simple template engine. If you’ve written templates for web pages before, this should feel familiar. The first part regenerates the original Person
struct, and the second part generates the From
method. We just plug the parsed parameters into the template.
// Regenerate original struct and conversion implementation let expanded = quote! { // Note: this generates the original struct `Person` pub struct #struct_name { #( #field_mappings, )* } impl From<#struct_name> for #target_type_tokens { fn from(item: #struct_name) -> #target_type_tokens { #target_type_tokens { #( #field_names: item.#field_names, )* } } } }; expanded.into()
Step 3: Test the Macro in a Project
First, compile the macro project with cargo build
. Then create a new test project:
cargo new test-mapstruct cd test-mapstruct
Modify the Cargo.toml
Dependencies
[dependencies] rust_mapstruct = { path = "../rust_mapstruct" }
Write a Simple Test in main.rs
use rust_mapstruct::auto_map; #[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // Use the auto-generated From implementation for conversion println!("dto: name:{}, age:{}", dto.name, dto.age); }
Run the Code and See the Result
In the test-mapstruct
project, run cargo build
, cargo run
, and see the result!
❯ cargo build Compiling test-mapstruct v0.1.0 (/home/maocg/study/test-mapstruct) Finished dev [unoptimized + debuginfo] target(s) in 0.26s test-mapstruct on master ❯ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/test-mapstruct` dto: name:Alice, age:30
We are Leapcell, your top choice for hosting Rust projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ