Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 column CustomerId (SeaORM Column enum) / customer_id (field name)
  • Parent struct VehiclePart → FK column VehiclePartId / 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_id instead of customer_id) are not yet supported via attributes. If your FK name doesn’t follow the convention, use a custom read::many::body hook 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() with join(one): Per-item queries (single entity, no batching needed)

Note: Batch loading currently requires UUID primary keys, consistent with the CRUDResource trait contract.

See Security for partial success and batch limit configuration.

See Also