Skip to content

Model serializers

ModelSerializer derives input validation and output representation from an Aksara model. It is useful for selecting fields and adding application validation. It does not authenticate callers, establish tenant membership, or make arbitrary ORM writes pass through the generated API policy boundary.

The ticket-desk tutorial supplies a complete serializer, model, migration, ViewSet wiring, and HTTP validation tests.

Select fields explicitly

For the tutorial's Ticket model, save this as app/serializers.py:

app/serializers.py
from aksara.api.serializers import ModelSerializer
from aksara.exceptions import ValidationError
from .models import Ticket


class TicketCreateSerializer(ModelSerializer):
    class Meta:
        model = Ticket
        fields = ["id", "subject", "description", "resolved"]
        read_only_fields = ["id", "resolved"]

    def validate_subject(self, value):
        subject = value.strip()
        if not subject:
            raise ValidationError(
                "Invalid ticket subject",
                errors={"subject": "A visible subject is required"},
            )
        return subject

Set create_serializer_class = TicketCreateSerializer on the ViewSet. There is no generic serializer_class switch on ModelViewSet; use the list, retrieve, create, or update-specific attribute for the operation you are customizing.

Validation and persistence

Construct a serializer with data=..., call synchronous is_valid(), inspect validated_data, then await serializer.save() when persistence is intended. For existing records, pass instance=.... The database must be connected and migrations applied before saving. For reads, .data or to_representation() returns the selected representation.

Validation first uses the generated Pydantic input model, then synchronous validate_<field>(value) hooks, then synchronous validate(data). Return the normalized value or dictionary from each hook. Do not make these hooks async.

is_valid() catches Pydantic validation errors and ValueError; with raise_exception=True it re-raises them. Aksara's separate ValidationError, used above for structured application errors, propagates to the generated router's HTTP 422 mapping. Do not assume every exception becomes a false return or that a plain application ValueError has the same HTTP response contract.

Supported configuration

Setting Meaning
Meta.model Required Aksara model
Meta.fields Explicit field list or "__all__"
Meta.exclude Exclude selected model fields
Meta.read_only_fields Omit fields from generated input validation
Meta.expand Explicit relationship representation configuration
context constructor argument Application context; generated ViewSets supply a request context
many=True Validate/represent multiple items; not a bulk-transaction guarantee

Meta.write_only_fields is not implemented in 0.7.0. Do not rely on it to hide passwords or other secrets from output. Use an explicit output field list or a dedicated response serializer and verify the actual response. Input-only secret handling often belongs in an application service, such as the account creation helper, rather than a raw user serializer.

Read-only input fields are omitted from the serializer's input model; this does not mean every extra client key is rejected. Generated HTTP/MCP execution adds its own field-write enforcement. A direct serializer caller must not treat successful validation as an authorization decision.

Partial updates need special care

The constructor accepts instance, data, many, and context. It does not accept a DRF-style partial=True argument. Input validation uses the serializer's full field requirements and dumps defaults into validated data. The default update method skips None values, so it is not a general “clear this nullable field” implementation either.

For ordinary PATCH behavior, retain the generated update schema and validate only explicitly supplied fields, as the ticket-desk update example does. If you introduce an update serializer, define and test omitted-field, default, null, and relationship replacement behavior explicitly. Do not reuse a create serializer and assume partial-update semantics.

Relationships and custom representation

Foreign keys use their stored identifier names in input, such as assigned_to_id. Meta.expand can request related output; ensure the relation is loaded through your query strategy. It does not promise automatic async queries during synchronous representation.

DRF-style SerializerMethodField, Field(source=...), and nested serializer attributes are not the declaration API shown by this implementation. Use the supported model fields/expansion or override to_representation() explicitly. The generated ViewSet response schema must also agree with the output; adding a dictionary key alone does not publish a new OpenAPI contract.

Saving multiple items or replacing relationships can perform several database operations. Use an explicit transaction when they must be atomic, and validate related-object tenant ownership before saving. Follow the tenant-isolation tutorial rather than assuming a foreign-key constraint proves authorization.