🔄

Zero-boilerplate struct field mapping for Rust

Skip to main content

Crate struct_mapper

Crate struct_mapper 

Source
Expand description

§struct-mapper

Derive macro to auto-generate impl From<Source> for Target by mapping struct fields.

Stop writing tedious manual From implementations for struct-to-struct conversions. struct-mapper generates them at compile time with zero runtime overhead.


§Overview

In Rust backend development with frameworks like Axum, Actix, or Rocket, you constantly need to convert between different struct representations — database entities to API responses, form inputs to domain models, and so on. This leads to dozens of repetitive impl From<A> for B blocks that are tedious to write and maintain.

struct-mapper eliminates this boilerplate with a single #[derive(MapFrom)] annotation.

§Quick Start

use struct_mapper::MapFrom;

// Your source struct (e.g., from database layer)
struct UserEntity {
    name: String,
    email: String,
    age: u32,
}

// Target struct — From<UserEntity> is auto-generated!
#[derive(Debug, MapFrom)]
#[map_from(UserEntity)]
struct UserResponse {
    name: String,
    email: String,
    age: u32,
}

let entity = UserEntity {
    name: "Khushi".to_string(),
    email: "khushi@gmail.com".to_string(),
    age: 30,
};

// Zero-boilerplate conversion!
let response: UserResponse = entity.into();
assert_eq!(response.name, "Khushi");
assert_eq!(response.email, "khushi@gmail.com");
assert_eq!(response.age, 30);

§Feature Guide

§1. Basic Mapping — Same Name, Same Type

When source and target fields have the same name and same type, no extra annotation is needed. The macro maps them automatically:

use struct_mapper::MapFrom;

struct Source { name: String, age: u32 }

#[derive(MapFrom)]
#[map_from(Source)]
struct Target { name: String, age: u32 }

let t: Target = Source { name: "Deendayal".into(), age: 25 }.into();
assert_eq!(t.name, "Deendayal");
assert_eq!(t.age, 25);

§2. Field Renaming — #[map(from = "...")]

When source and target use different field names, use from to specify which source field to read from:

use struct_mapper::MapFrom;

struct DbRow {
    user_name: String,
    user_age: u32,
}

#[derive(MapFrom)]
#[map_from(DbRow)]
struct ApiUser {
    #[map(from = "user_name")]
    name: String,
    #[map(from = "user_age")]
    age: u32,
}

let row = DbRow { user_name: "Anjali".into(), user_age: 35 };
let user: ApiUser = row.into();
assert_eq!(user.name, "Anjali");
assert_eq!(user.age, 35);

§3. Skip + Default — #[map(skip, default)]

For target fields that don’t exist in the source, mark them as skipped. They will be filled with Default::default():

use struct_mapper::MapFrom;

struct Entity { name: String }

#[derive(MapFrom)]
#[map_from(Entity)]
struct Response {
    name: String,
    #[map(skip, default)]
    request_id: String,    // → "" (String::default())
    #[map(skip, default)]
    retry_count: u32,      // → 0 (u32::default())
}

let r: Response = Entity { name: "Nikhil".into() }.into();
assert_eq!(r.name, "Nikhil");
assert_eq!(r.request_id, "");
assert_eq!(r.retry_count, 0);

Note: skip always requires default. Using #[map(skip)] alone will produce a clear compile error telling you to add default.

§4. Nested Conversion — #[map(into)]

When a source field’s type implements Into<TargetFieldType>, use into to automatically call .into() during conversion:

use struct_mapper::MapFrom;

struct AddressEntity { city: String }

#[derive(Debug, PartialEq, MapFrom)]
#[map_from(AddressEntity)]
struct AddressDTO { city: String }

struct OrderEntity {
    id: u64,
    address: AddressEntity,
}

#[derive(Debug, MapFrom)]
#[map_from(OrderEntity)]
struct OrderDTO {
    id: u64,
    #[map(into)]
    address: AddressDTO,  // source.address.into()
}

let order = OrderEntity {
    id: 42,
    address: AddressEntity { city: "Springfield".into() },
};
let dto: OrderDTO = order.into();
assert_eq!(dto.id, 42);
assert_eq!(dto.address.city, "Springfield");

§5. Custom Function — #[map(with = "...")]

For complex transformations, pass a function path that takes the source field value and returns the target field type:

use struct_mapper::MapFrom;

fn cents_to_dollars(cents: u64) -> f64 {
    cents as f64 / 100.0
}

struct PriceEntity { amount_cents: u64 }

#[derive(MapFrom)]
#[map_from(PriceEntity)]
struct PriceDTO {
    #[map(from = "amount_cents", with = "cents_to_dollars")]
    amount: f64,
}

let dto: PriceDTO = PriceEntity { amount_cents: 1999 }.into();
assert!((dto.amount - 19.99).abs() < f64::EPSILON);

§6. Combining Attributes

All field attributes can be combined freely:

use struct_mapper::MapFrom;

fn to_upper(s: String) -> String { s.to_uppercase() }

struct Source {
    id: u64,
    user_name: String,
    raw_email: String,
}

#[derive(Debug, MapFrom)]
#[map_from(Source)]
struct Target {
    id: u64,                                       // direct
    #[map(from = "user_name")]
    name: String,                                  // renamed
    #[map(from = "raw_email", with = "to_upper")]
    email: String,                                 // renamed + custom fn
    #[map(skip, default)]
    request_id: String,                            // skipped
}

let t: Target = Source {
    id: 1,
    user_name: "Sulochan".into(),
    raw_email: "sulochan@gmail.com".into(),
}.into();

assert_eq!(t.id, 1);
assert_eq!(t.name, "Sulochan");
assert_eq!(t.email, "SULOCHAN@GMAIL.COM");
assert_eq!(t.request_id, "");

§Attribute Reference

§Struct-Level

AttributeRequiredDescription
#[map_from(Type)]YesSpecifies the source type for From<Type> generation

§Field-Level

AttributeDescription
#[map(from = "name")]Map from a differently-named source field
#[map(skip, default)]Skip this field; use Default::default()
#[map(into)]Call .into() on the source field value
#[map(with = "path")]Apply a conversion function fn(SourceFieldType) -> TargetFieldType

Attributes can be combined: #[map(from = "old", with = "convert")]


§Error Messages

struct-mapper provides clear, actionable compile errors:

  • Missing #[map_from]: Tells you exactly which attribute to add, with an example.
  • #[map(skip)] without default: Shows the fix: #[map(skip, default)].
  • Contradictory attributes: Explains why from + skip can’t be combined.
  • Non-struct usage: Explains that only named-field structs are supported.

§How It Works

The #[derive(MapFrom)] macro:

  1. Parses the #[map_from(Source)] attribute to find the source type.
  2. Inspects each field’s #[map(...)] attributes.
  3. Generates an impl From<Source> for Target block with the correct field assignments — direct copies, renames, .into() calls, or function applications.

All work happens at compile time. The generated code is identical to what you would write by hand — zero runtime overhead, zero allocations, zero dependencies.


§Limitations

v0.2 constraints:

  • Only named-field structs. Tuple structs and enums are not yet supported.
  • Generic target structs work; generic source types need manual handling.

Structs§

MapError
Error type for fallible struct mapping via TryMapFrom.

Derive Macros§

MapFrom
Derive macro that generates impl From<Source> for Target by mapping struct fields.
TryMapFrom
Derive macro that generates impl TryFrom<Source> for Target by mapping struct fields.