A quick and easy way to create derivative models of your existing types without repeating yourself. Reduce boilerplate and automatically generate related structs with custom field subsets and transformations.
Features
- Reduce boilerplate: Generate multiple related structs from a single definition
- Flexible field selection: Include or exclude specific fields with
fields()andomit() - Automatic trait generation:
From<T>implementations between original and generated structs - Derive support: Apply derives to generated structs
- Multiple model types: Views, patches, and custom transformations
New features and roadmap are available here on GitHub.
Installation
Add restructed to your Cargo.toml:
[dependencies] restructed = "0.2"
Or run this command in your project directory:
Quick Start
Add the derive macro to your struct:
use restructed::Models; #[derive(restructed::Models)] struct User { id: i32, username: String, email: String, password: String, }
Then add attributes for each model you want to generate:
#[derive(restructed::Models)] #[view(UserProfile, omit(password))] // Subset without sensitive fields #[patch(UserUpdate, fields(username, email))] // Optional fields for updates struct User { id: i32, username: String, email: String, password: String, }
This generates:
UserProfilestruct withid,username, andemailfieldsUserUpdatestruct withOption<String>fields forusernameandemail- Automatic
Fromimplementations for conversions
Model Types
#[view] - Field Subsets
Creates a struct containing a subset of the original fields. Perfect for API responses, database views, or public representations.
Arguments:
| Name | Description | Required | Type | Example |
|---|---|---|---|---|
name |
Name of the generated struct | Yes (first) | Identifier | UserProfile |
fields |
Fields to include | No | List | fields(id, username) |
omit |
Fields to exclude | No | List | omit(password, secret) |
derive |
Traits to derive | No | List | derive(Debug, Clone) |
preset |
Behavior preset to apply | No | String | preset = "read" |
attributes_with |
Attributes to inherit | No | String | attributes_with = "all" |
Note: Use either fields OR omit, not both.
Example:
#[derive(Clone, restructed::Models)] #[view(UserProfile, omit(id, password))] struct User { id: i32, // Not in UserProfile username: String, // In UserProfile email: String, // In UserProfile bio: String, // In UserProfile password: String, // Not in UserProfile } // Usage let user = User { id: 1, username: "alice".to_string(), email: "alice@example.com".to_string(), bio: "Rustacean".to_string(), password: "super_secret".to_string(), }; let profile: UserProfile = user.into();
#[patch] - Optional Field Wrappers
Creates a struct where each field is wrapped in Option<T>. Ideal for partial updates, PATCH endpoints, or optional modifications.
Arguments:
| Name | Description | Required | Type | Example |
|---|---|---|---|---|
name |
Name of the generated struct | Yes (first) | Identifier | UserUpdate |
fields |
Fields to include | No | List | fields(username, email) |
omit |
Fields to exclude | No | List | omit(id, created_at) |
derive |
Traits to derive | No | List | derive(Debug, Serialize) |
preset |
Behavior preset to apply | No | String | preset = "write" |
attributes_with |
Attributes to inherit | No | String | attributes_with = "oai" |
option |
Alternative to Option<T> |
No | Type | option = MaybeUndefined |
skip_serializing_double_option |
Skip serializing None for Option<Option<T>> |
No | Boolean | skip_serializing_double_option = true |
Example:
#[derive(Clone, restructed::Models)] #[patch(UserUpdate, omit(id))] struct User { id: i32, // Not in UserUpdate username: String, // Option<String> in UserUpdate email: String, // Option<String> in UserUpdate bio: Option<String>, // Option<Option<String>> in UserUpdate } // Usage let update = UserUpdate { username: Some("new_username".to_string()), email: None, // Don't update email bio: Some(Some("New bio".to_string())), // Set bio };
#[model] - Base Configuration
Defines default arguments applied to all generated models. This attribute doesn't generate structs itself but configures other model generators.
Sub-attributes:
base - Non-overridable Defaults
Arguments that are always applied and cannot be overridden by individual models.
#[model(base(derive(Debug, Clone)))] // All models MUST derive Debug and Clone
defaults - Overridable Defaults
Arguments applied only when not specified by individual models.
#[model(defaults(derive(Serialize), preset = "read"))] // Applied unless overridden
Example:
#[derive(restructed::Models)] #[model( base(derive(Debug)), // All models derive Debug defaults(derive(Clone), preset = "read") // Default unless overridden )] #[view(UserView)] // Inherits Debug + Clone + preset="read" #[patch(UserPatch, preset = "write")] // Inherits Debug + Clone, overrides preset struct User { id: i32, username: String, password: String, }
Advanced Features
Presets
Presets apply common configurations automatically:
"none"(default): No special behavior"write"(requires 'openapi' feature): For writable fields- Removes
#[oai(read_only)]fields - Uses
MaybeUndefinedfor patch option type
- Removes
"read"(requires 'openapi' feature): For readable fields- Removes
#[oai(write_only)]fields - Uses
MaybeUndefinedfor patch option type
- Removes
Attribute Inheritance
Control which attributes are copied to generated structs:
-
"none"(default): No attributes copied -
"oai"(requires 'openapi' feature): Copy OpenAPI attributes -
"deriveless": Copy all attributes except derives -
"all": Copy all attributes, even dervies but they'll need to be on their own line (See below example) i.e.#[derive(restructed::Models)] #[model(defaults(attributes_with = "all"))] #[derive(Clone)]
Complete Example
#[derive(restructed::Models, Clone)] #[view(UserProfile, omit(password, internal_id))] #[view(UserSummary, fields(id, username))] #[patch(UserUpdate, omit(id, internal_id, created_at), preset = "write")] struct User { id: i32, internal_id: String, username: String, email: String, password: String, created_at: String, } fn example_usage() { let user = User { id: 1, internal_id: "internal_123".to_string(), username: "alice".to_string(), email: "alice@example.com".to_string(), password: "secret".to_string(), created_at: "2024-01-01".to_string(), }; // Convert to different views let profile: UserProfile = user.clone().into(); let summary: UserSummary = user.clone().into(); // Create update struct let update = UserUpdate { username: Some("new_alice".to_string()), email: None, // Don't update password: Some("new_secret".to_string()), }; }
Feature Flags
openapi - Poem OpenAPI Integration
Enables integration with the poem-openapi crate:
- Use
MaybeUndefined<T>instead ofOption<T>in patch models - Copy
#[oai(...)]attributes to generated structs - Respect
read_only/write_onlyattributes in presets
use restructed::Models; #[derive(poem_openapi::Object, Models)] #[oai(skip_serializing_if_is_none, rename_all = "camelCase")] #[model( base(derive(poem_openapi::Object, Debug)), defaults(preset = "read") )] #[patch(UserUpdate, preset = "write")] #[view(UserProfile)] #[view(UserNames, fields(username, name, surname))] pub struct User { #[oai(read_only)] pub id: u32, #[oai(validator(min_length = 3, max_length = 16, pattern = r"^[a-zA-Z0-9_]*$"))] pub username: String, #[oai(validator(min_length = 5, max_length = 1024), write_only)] pub password: String, #[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))] pub name: Option<String>, #[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))] pub surname: Option<String>, #[oai(read_only)] pub joined: u64, }
Limitations
- Generic types: Currently doesn't support generic structs or enums (e.g.,
Struct<T>) - Enum support: Only works with structs, not enums
Contributions for these features are welcome!
License
See the project repository for license information.