Derive macro to auto-generate From<Source> and TryFrom<Source> for your structs
— zero boilerplate field mapping.
📖 Documentation · 📦 Crates.io · 🐛 Report Bug · 💡 Request Feature
Stop writing tedious manual From and TryFrom implementations for struct-to-struct conversions. struct-mapper generates them at compile time with zero runtime overhead.
use struct_mapper::MapFrom;
struct UserEntity {
name: String,
email: String,
age: u32,
}
#[derive(MapFrom)]
#[map_from(UserEntity)]
struct UserResponse {
name: String,
email: String,
age: u32,
}
// That's it! Now you can do:
let entity = UserEntity { name: "Alice".into(), email: "a@b.com".into(), age: 30 };
let response: UserResponse = entity.into();No runtime cost. No reflection. Just a clean
impl From<>generated at compile time.
Every Rust backend developer writes this dozens of times:
| 😩 Before — Manual boilerplate | 🚀 After — One derive |
|---|---|
impl From<UserEntity> for UserResponse {
fn from(e: UserEntity) -> Self {
UserResponse {
name: e.name,
email: e.email,
age: e.age,
display_name: e.first_name,
address: e.address.into(),
created_at: Default::default(),
}
}
} |
#[derive(MapFrom)]
#[map_from(UserEntity)]
struct UserResponse {
name: String,
email: String,
age: u32,
#[map(from = "first_name")]
display_name: String,
#[map(into)]
address: AddressResponse,
#[map(skip, default)]
created_at: String,
} |
Add to your Cargo.toml:
[dependencies]
struct-mapper = "0.2"Minimum Supported Rust Version: 1.71.0
Fields with matching names are mapped automatically. No attributes needed.
use struct_mapper::MapFrom;
struct Source {
name: String,
age: u32,
}
#[derive(MapFrom)]
#[map_from(Source)]
struct Target {
name: String,
age: u32,
}
let target: Target = Source { name: "Alice".into(), age: 30 }.into();
assert_eq!(target.name, "Alice");When source and target field names differ:
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,
}For fields that don't exist in the source struct:
use struct_mapper::MapFrom;
struct Entity {
name: String,
}
#[derive(MapFrom)]
#[map_from(Entity)]
struct Response {
name: String,
#[map(skip, default)]
request_id: String, // → Default::default() = ""
#[map(skip, default)]
retry_count: u32, // → Default::default() = 0
}For fields where the source type implements Into<TargetType>:
use struct_mapper::MapFrom;
struct AddressEntity { street: String, city: String }
#[derive(MapFrom)]
#[map_from(AddressEntity)]
struct AddressDTO { street: String, city: String }
struct OrderEntity {
id: u64,
address: AddressEntity,
}
#[derive(MapFrom)]
#[map_from(OrderEntity)]
struct OrderDTO {
id: u64,
#[map(into)]
address: AddressDTO, // → source.address.into()
}For complex transformations using any function:
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 PriceResponse {
#[map(from = "amount_cents", with = "cents_to_dollars")]
amount: f64,
}All attributes work together seamlessly:
use struct_mapper::MapFrom;
struct OrderEntity {
order_id: u64,
user_name: String,
total_cents: u64,
address: AddressEntity,
}
#[derive(MapFrom)]
#[map_from(OrderEntity)]
struct OrderResponse {
order_id: u64, // direct
#[map(from = "user_name")]
name: String, // renamed
#[map(from = "total_cents", with = "cents_to_dollars")]
total: f64, // renamed + custom fn
#[map(into)]
address: AddressDTO, // nested conversion
#[map(skip, default)]
request_id: String, // skipped
}When conversions can fail (type narrowing, parsing, validation), use TryMapFrom:
use struct_mapper::TryMapFrom;
use std::num::ParseIntError;
fn parse_port(s: String) -> Result<u16, ParseIntError> {
s.parse::<u16>()
}
struct RawConfig {
port_str: String,
max_conn: i64,
host: String,
}
#[derive(TryMapFrom)]
#[try_map_from(RawConfig)]
struct ValidConfig {
#[map(from = "port_str", try_with = "parse_port")]
port: u16, // fallible: string → u16
#[map(try_into)]
max_conn: u32, // fallible: i64 → u32
host: String, // direct (infallible)
}
// Success:
let raw = RawConfig { port_str: "8080".into(), max_conn: 100, host: "localhost".into() };
let config: ValidConfig = raw.try_into().unwrap();
// Failure — tells you exactly which field failed:
let bad = RawConfig { port_str: "not_a_port".into(), max_conn: 100, host: "x".into() };
let err = ValidConfig::try_from(bad).unwrap_err();
assert_eq!(err.field, "port");
println!("{}", err); // "mapping failed at field `port`: invalid digit found in string"| Attribute | Applies To | Description |
|---|---|---|
#[map_from(Type)] |
Struct | Source type to generate From<Type> for |
#[try_map_from(Type)] |
Struct | Source type to generate TryFrom<Type> for |
#[map(from = "name")] |
Field | Map from a differently-named source field |
#[map(skip, default)] |
Field | Skip this field, use Default::default() |
#[map(into)] |
Field | Call .into() on the source value |
#[map(with = "fn")] |
Field | Apply a custom conversion function |
#[map(try_into)] |
Field | Call .try_into() on the source value (TryMapFrom only) |
#[map(try_with = "fn")] |
Field | Apply a fallible function (TryMapFrom only) |
💡 Tip: Attributes can be combined:
#[map(from = "old_name", try_with = "parse_fn")]
struct-mapper provides clear, actionable error messages that tell you exactly what went wrong and how to fix it:
error: missing `#[map_from(SourceType)]` attribute.
Add `#[map_from(YourSourceStruct)]` to specify which struct to map from.
Example:
#[derive(MapFrom)]
#[map_from(UserEntity)]
struct UserResponse { ... }
error: `#[map(skip)]` requires `#[map(skip, default)]`.
When skipping a field, you must provide a default value.
Fix: #[map(skip, default)]
How does struct-mapper compare to alternatives?
| Feature | struct-mapper | derive-into | more-convert | structural-convert |
|---|---|---|---|---|
| Same-field mapping | ✅ | ✅ | ✅ | ✅ |
| Field renaming | ✅ | ✅ | ✅ | |
| Skip + default | ✅ | |||
Nested .into() |
✅ | ✅ | ❌ | |
| Custom function | ✅ | ✅ | ||
TryFrom support |
✅ | ❌ | ❌ | ❌ |
| Fallible custom fn | ✅ | ❌ | ❌ | ❌ |
| Clear error messages | ✅ | ❌ | ❌ | ❌ |
| Clean syntax | ✅ | |||
| Compile-time only | ✅ | ✅ | ✅ | ✅ |
| Zero runtime deps | ✅ | ✅ | ✅ | ✅ |
-
From— infallible struct conversion - Field renaming, skipping, nesting, custom functions
- Clear compile-time error messages
-
TryFrom— fallible conversions (v0.2) ✅ - Enum variant mapping (
v0.3) - Bi-directional mapping (
v0.4)
- Only named struct fields. Tuple structs and enums are not yet supported.
- Generics on the target struct are supported; generic source types require manual annotation.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
|
Deendayal Kumawat |
Licensed under either of:
- Apache License, Version 2.0 — LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0
- MIT License — LICENSE-MIT or http://opensource.org/licenses/MIT
at your option.
⭐ If you find this useful, please consider giving it a star! ⭐
Made with ❤️ in Rust