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:
skipalways requiresdefault. Using#[map(skip)]alone will produce a clear compile error telling you to adddefault.
§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
| Attribute | Required | Description |
|---|---|---|
#[map_from(Type)] | Yes | Specifies the source type for From<Type> generation |
§Field-Level
| Attribute | Description |
|---|---|
#[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)]withoutdefault: Shows the fix:#[map(skip, default)].- Contradictory attributes: Explains why
from+skipcan’t be combined. - Non-struct usage: Explains that only named-field structs are supported.
§How It Works
The #[derive(MapFrom)] macro:
- Parses the
#[map_from(Source)]attribute to find the source type. - Inspects each field’s
#[map(...)]attributes. - Generates an
impl From<Source> for Targetblock 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 Targetby mapping struct fields. - TryMap
From - Derive macro that generates
impl TryFrom<Source> for Targetby mapping struct fields.