Skip to content

Validation

Validation checks whether input is acceptable. It is separate from deciding who may write, which tenant owns a record, and whether concurrent writes can violate an invariant.

Choose the validation boundary

Requirement Supported approach Limit
Field shape, size, or range Type-specific field options Constructor options differ by field type.
Normalize API input or compare supplied fields Synchronous ModelSerializer hooks Not a hook for asynchronous database queries.
Check a related row's tenant or ownership Explicit application checks in the write path Validation must use current trusted identity and tenant context.
Keep an invariant true under concurrent writes PostgreSQL constraints and appropriate transactions/locking A prior lookup alone cannot prevent a race.
Normalize an individual model save An explicit save override or pre_save receiver Does not automatically cover queryset, bulk, or raw writes.

Field-level validation

Use supported constructor parameters such as String(max_length=...), String(min_length=...), String(regex=...), and Integer(min_value=..., max_value=...). String, Integer, and supported integer variants also accept choices. These options are not universal base-field parameters. Use nullable=True, not null=True.

There is no general aksara.validation.Validator API or universal validators=[...]/error_messages={...} field contract. A constructor accepting an option on one field does not establish support on other types.

Individual Model.save() prepares and validates fields before writing. Bulk operations and queryset writes have their own behavior; see bulk operations. Database constraints remain necessary for rules that must hold across every write path.

Serializer hooks

Define synchronous validate_<field>(value) methods and validate(data). Return the normalized value or dictionary. Do not use async def for these hooks: the serializer does not await them. The serializer guide provides an executable TicketCreateSerializer, including blank-subject rejection and error behavior.

For partial updates, distinguish absent fields from supplied values and from explicit None. ModelSerializer(partial=True) is not supported. The ticket desk tutorial demonstrates custom validation on the generated PATCH path without assuming a DRF serializer contract.

Reusable normalization can be an ordinary Python function called explicitly from a hook. It does not need a framework decorator or base validator class. Keep database I/O in an explicit async application operation, outside the synchronous validation hook.

Cross-field and database-dependent checks

A method named clean() is not automatically invoked by the base Model. If an application defines one, it must call it explicitly in each relevant write path. Overriding save() only covers callers that use that method.

For database-dependent rules, first validate input shape, then resolve related records and authorize the operation on the server. Perform the mutation inside the transaction or locking boundary required by the invariant. The tenant chapter checks that a related Agent belongs to the same tenant as the Ticket; a client-supplied identifier does not prove that relationship.

A uniqueness lookup can give a helpful early error, but another writer may insert before the current write. Apply a unique constraint through migrations and handle its database error. Likewise, reading an object's current status before validation does not make a state transition atomic with a later save.

Validation errors

Use the actual error type for the layer being handled. Aksara's aksara.exceptions.ValidationError supports field errors, while Pydantic validation errors follow Pydantic's contract. ModelSerializer.is_valid() is synchronous; custom Aksara validation errors may propagate rather than being converted to a false result. The serializer reference explains this distinction and the generated HTTP 422 path.

Custom FastAPI handlers must explicitly map their errors and enforce permissions. Returning (body, 400) does not set an HTTP status under the framework's response contract. See custom actions before adding a write endpoint.

Test the rule where it matters

Test accepted and rejected inputs through the actual entry point: a direct validator unit test alone does not prove a ViewSet calls it. Include omitted fields, explicit nulls, another tenant's identifiers, and concurrent writes when those cases affect the invariant. Keep authentication, permissions, and field-write policy tests alongside validation tests; valid data can still be unauthorized.