Bringing Java's MapStruct to Rust

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 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::(&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! { //

Apr 21, 2025 - 20:39
 0
Bringing Java's MapStruct to Rust

Cover

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:

  1. Extract the "target" parameter from the macro.
  2. Parse the input struct (Person).
  3. Extract the field names and types from the input struct.
  4. Parse the target type.
  5. 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

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!

Try Leapcell

Follow us on X: @LeapcellHQ

Read on our blog