Struct Attributes Reference
Complete reference for #[crudcrate(...)] attributes on structs.
Syntax
#[derive(EntityToModels)]
#[crudcrate(attribute1, attribute2 = value, ...)]
pub struct Model { }
Attributes
generate_router
Generates an Axum router function for all CRUD operations.
#[crudcrate(generate_router)]
pub struct Model { }
// Generates:
pub fn model_router() -> Router { }
Type: Flag (no value)
routes
Names the route families router() mounts, out of create, read, update
and delete. Each family covers its single-row and batch routes. An operation
left out is not a route at all, so the OpenAPI document does not advertise it.
#[crudcrate(generate_router, routes(read, update))]
pub struct Model { }
// Mounts GET /, GET /{id}, PATCH /{id} and PATCH /batch.
Omit the attribute for every route, which is the default. Use it for a table whose rows are a projection of something else, where a create or delete has no legitimate meaning.
Type: List of identifiers
upsert_key
Names the alternate unique key a source system registers rows under, in the
order the index declares them. Enables crudcrate::upsert.
#[crudcrate(upsert_key(source_system, source_key))]
pub struct Model { }
Type: List of field names
upsert_where
The predicate the upsert_key is unique under, where the index backing it is
partial. A SeaORM expression, the same value OnConflict::target_cond_where
takes; the registration’s find adds it, so a row the index does not cover is
never resolved as the key’s row. Without it the key is read as unique over
every row. Naming it without an upsert_key is a compile error.
#[crudcrate(upsert_key(site_id, parameter_id), upsert_where = Column::ResolvedAt.is_null())]
pub struct Model { }
Type: Expression
api_struct
Override the name of generated API structs.
#[crudcrate(api_struct = "Product")]
pub struct Model { }
// Generates: Product, ProductCreate, ProductUpdate, ProductList
Type: String literal Default: Derives from table name (e.g., “products” → “Product”)
name_singular
Override the singular resource name for routing and headers.
#[crudcrate(name_singular = "person")]
pub struct Model { }
// Used in: Content-Range header
// Content-Range: person 0-9/100
Type: String literal Default: Lowercase struct name
name_plural
Override the plural resource name for routing.
#[crudcrate(name_plural = "people")]
pub struct Model { }
// Routes: GET /people, POST /people, etc.
Type: String literal
Default: {name_singular}s
operations
Specify a custom CRUDOperations implementation.
#[crudcrate(operations = MyOperations)]
pub struct Model { }
// MyOperations must implement CRUDOperations trait
pub struct MyOperations;
impl CRUDOperations for MyOperations {
type Resource = Model;
// ...
}
Type: Type path
Default: DefaultCRUDOperations<Self>
description
Add description for OpenAPI documentation.
#[crudcrate(description = "Blog articles with comments")]
pub struct Model { }
Type: String literal Default: None
fulltext_language
Set language for PostgreSQL fulltext search.
#[crudcrate(fulltext_language = "spanish")]
pub struct Model { }
Type: String literal
Default: "english"
Options: "english", "spanish", "german", "french", "simple", etc.
batch_limit
Set the maximum number of items for batch create/update/delete operations.
#[crudcrate(batch_limit = 500)]
pub struct Model { }
Type: Integer
Default: 100
Runtime override: Implement fn batch_limit() -> usize on your CRUDResource impl for dynamic values (env vars, config).
max_page_size
Set the maximum items per page for pagination.
#[crudcrate(max_page_size = 500)]
pub struct Model { }
Type: Integer
Default: 1000
Runtime override: Implement fn max_page_size() -> u64 on your CRUDResource impl for dynamic values.
max_child_rows
Caps the number of child rows one join field may load in a single request. Exceeding it returns 413 Payload Too Large instead of an unbounded response.
#[crudcrate(max_child_rows = 500)] // default unlimited
Type: Integer
Runtime override: Set max_child_rows_per_relation on the SecurityProfile returned by fn security_profile() on your CRUDResource impl.
deny_unknown_fields
Reject a create or update payload that carries a key the input model does not accept, instead of ignoring it.
#[crudcrate(deny_unknown_fields)]
pub struct Model {
#[crudcrate(exclude(create))]
pub is_public: Option<bool>,
}
// POST { "name": "x", "is_public": true } -> 422, is_public is not a create field
By default serde ignores keys with no matching field, so a value excluded from the
Create or Update model is dropped and the caller gets a 201/200 reporting the
stored value rather than the one it sent. The flag adds
#[serde(deny_unknown_fields)] to the generated <Name>Create and <Name>Update
structs, turning that silent drop into a rejection.
Use it when clients should learn that a field is server-controlled. Leave it off
when clients round-trip a whole record back into an update, since read-only fields
like created_at would then be rejected too.
Type: Flag (no value) Default: off (unknown keys ignored)
Lifecycle Hook Attributes
create::one::pre
Function called before create operation.
#[crudcrate(create::one::pre = validate_create)]
async fn validate_create<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
data: &mut ModelCreate,
) -> Result<(), ApiError> { }
create::one::post
Function called after successful create.
#[crudcrate(create::one::post = notify_created)]
async fn notify_created<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
created: &Model,
) -> Result<(), ApiError> { }
create::one::body
Replace entire create logic.
#[crudcrate(create::one::body = custom_create)]
async fn custom_create<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
data: ModelCreate,
) -> Result<Model, ApiError> { }
update::one::pre
Function called before update operation.
#[crudcrate(update::one::pre = check_update_permission)]
async fn check_update_permission<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: PrimaryKeyType,
data: &mut ModelUpdate,
) -> Result<(), ApiError> { }
update::one::post
Function called after successful update.
#[crudcrate(update::one::post = invalidate_cache)]
async fn invalidate_cache<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
updated: &Model,
) -> Result<(), ApiError> { }
update::one::body
Replace entire update logic.
#[crudcrate(update::one::body = custom_update)]
async fn custom_update<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: PrimaryKeyType,
data: ModelUpdate,
) -> Result<Model, ApiError> { }
delete::one::pre
Function called before delete operation.
#[crudcrate(delete::one::pre = check_delete_permission)]
async fn check_delete_permission<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: PrimaryKeyType,
) -> Result<(), ApiError> { }
delete::one::post
Function called after successful delete.
#[crudcrate(delete::one::post = cleanup_related)]
async fn cleanup_related<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: PrimaryKeyType,
) -> Result<(), ApiError> { }
delete::one::body
Replace entire delete logic (e.g., for soft delete).
#[crudcrate(delete::one::body = soft_delete)]
async fn soft_delete<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: PrimaryKeyType,
) -> Result<(), ApiError> { }
get::one::pre
Function called before get_one operation.
#[crudcrate(get::one::pre = check_view_permission)]
async fn check_view_permission<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: PrimaryKeyType,
) -> Result<(), ApiError> { }
get::all::pre
Function called before get_all operation, can modify condition.
#[crudcrate(get::all::pre = filter_by_tenant)]
async fn filter_by_tenant<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
condition: &mut Condition,
) -> Result<(), ApiError> { }
Complete Example
#[derive(EntityToModels)]
#[crudcrate(
generate_router,
api_struct = "Article",
name_singular = "article",
name_plural = "articles",
operations = ArticleOperations,
description = "Blog articles with comments",
fulltext_language = "english",
create::one::pre = validate_article,
create::one::post = index_for_search,
update::one::pre = check_edit_permission,
delete::one::body = soft_delete_article,
get::all::pre = filter_published_only,
)]
#[sea_orm(table_name = "articles")]
pub struct Model {
// ...
}
Foreign Key Naming Convention
When using join() for batch loading in get_all(), CRUDCrate derives the foreign key column name from the parent struct name using PascalCase convention:
- Parent struct
Customer→ FK columnCustomerId(SeaORM Column enum) /customer_id(field name) - Parent struct
VehiclePart→ FK columnVehiclePartId/vehicle_part_id
Your related entity’s SeaORM model must have a matching foreign key field. For example, if Customer has vehicles: Vec<Vehicle>, the Vehicle model must have a customer_id: Uuid field and a Column::CustomerId variant.
Note: Custom FK names (e.g.,
owner_idinstead ofcustomer_id) are not yet supported via attributes. If your FK name doesn’t follow the convention, use a customread::many::bodyhook to implement the loading logic.
Batch Loading Query Behavior
For get_all() with join(all) or join(one, all), CRUDCrate uses batch loading:
- Depth=1 joins: 2 queries total (1 for parents + 1 per join field using
WHERE fk IN (...)) - Depth > 1 joins: Additional per-item queries for nested children (falls back to
get_one()calls) get_one()withjoin(one): Per-item queries (single entity, no batching needed)
Note: Batch loading currently requires UUID primary keys, consistent with the
CRUDResourcetrait contract.
See Security for partial success and batch limit configuration.