---
url: "https://vueda.dev/v3/guides/split-read-write-serializers.md"
title: "Split Read/Write Serializers Safely"
---

# Split Read/Write Serializers Safely

This guide sets up one viewset that serializes `list` and `retrieve` with a read serializer, and `create`, `update`, and `partial_update` with a write serializer. Use the split when reads need computed or nested data that writes do not accept, or when write validation needs other fields. The steps keep the default views working, including the redirect after a save.

## Before You Start

The model needs a [Canonical Registration](/v3/reference/glossary.md#canonical-registration) with a viewset. [Model Info](/v3/reference/glossary.md#model-info) builds its field and expand sections from the [Canonical Serializer](/v3/reference/glossary.md#canonical-serializer) only, as [Canonical Registration and Model Discovery](/v3/core-concepts/canonical-registration-and-discovery.md#viewset-presence-and-metadata-completeness) describes. Per-action serializers do not change model info. So every view's default [View Field Lists](/v3/reference/glossary.md#view-field-lists) and default `expand` come from the serializer that you register, whichever action serves the request.

That page also describes what a model registered with [info.registration.register\_serializer](/v3/reference/api/py/vueda.info.registration.md#register_serializer) lacks. With no viewset, model info lists no actions, and the client blocks every route for the model.

## Register the Canonical Serializer

Register the serializer whose fields the forms should show. This is usually the write serializer, because the create and update forms submit the fields that model info lists. Pass the viewset to [info.registration.register](/v3/reference/api/py/vueda.info.registration.md#register):

```python
from django.apps import AppConfig


class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        from vueda.info.registration import register

        from .serializers import MyWriteSerializer
        from .viewsets import MyViewSet

        register(MyWriteSerializer, MyViewSet)
```

## Map Actions to Serializers

Add [PerActionSerializerMixin](/v3/reference/api/py/vueda.core.viewsets/PerActionSerializerMixin.md) to the viewset. Set an `<action>_serializer_class` attribute for each action:

```python
from vueda.core.viewsets import PerActionSerializerMixin
from vueda.core.viewsets import VuedaViewSet


class MyViewSet(PerActionSerializerMixin, VuedaViewSet):
    serializer_class = MyWriteSerializer
    list_serializer_class = MyReadSerializer
    retrieve_serializer_class = MyReadSerializer
    create_serializer_class = MyWriteSerializer
    update_serializer_class = MyWriteSerializer
    partial_update_serializer_class = MyWriteSerializer
```

The mixin returns the class that the current action's attribute names. An action with no attribute uses [`serializer_class`](https://www.django-rest-framework.org/api-guide/generic-views/#attributes).

- [VuedaViewSet](/v3/reference/api/py/vueda.core.viewsets/VuedaViewSet.md) does not include the mixin, so add it yourself.
- List the mixin before `VuedaViewSet`. When the mixin comes after `VuedaViewSet`, DRF's [`get_serializer_class`](https://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself) answers first, and the mixin never runs.
- Set `partial_update_serializer_class` as well as `update_serializer_class`. A PATCH request runs the `partial_update` action. Without its own attribute, that action uses `serializer_class`.

## Match the Fields Each View Requests

The default views send canonical field names in the `f` query parameter and canonical expand names in `e`. Each action's serializer must answer those names. [Field and Expand Semantics](/v3/core-concepts/field-and-expand-semantics.md) describes how `f`, `e`, and `om` behave on reads and writes.

**Reads.** `list` and `retrieve` check `f` and `e` against the read serializer. A canonical field or expand that the read serializer lacks fails the request with `400`. [ViewUpdate](/v3/reference/api/vue/ViewUpdate.md) loads its object with `retrieve`, so the update view fails the same way. Declare every canonical field and expand on the read serializer.

A `list` request accepts no expand until the viewset sets `permit_list_expands`. [Use Expand and Sparse Field Controls](/v3/guides/expand-and-fields-controls.md) gives the steps.

**Writes.** A create or update save sends the view's `fetchFields` and the pk in `f`. A save also sends in `e` each configured expand whose form value is set. The write action checks both against the write serializer, with the same rules as a read. A field or expand that the write serializer lacks, or an expand that the action's permit list leaves out, fails the save with `400`. The write action answers before it validates the body, so it saves nothing. Declare every fetched field and every configured expand on the write serializer, or narrow the view's `fetchFields`.

**The pk.** [ViewCreate](/v3/reference/api/vue/ViewCreate.md) redirects to the update view by default. That redirect, and a redirect to the read view, take the pk from the save response. Include the pk in the write serializer's `Meta.fields`.

To narrow what one view requests, set its `fetchFields` prop, such as [`fetchFields`](/v3/reference/api/vue/ViewUpdate.md#prop-fetchFields) on `ViewUpdate`. A per-view config that you set with [setConfig](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md) works too. The update view uses one `fetchFields` list for its retrieve and its save, so both serializers must declare those fields.

## Check the Split

1. Request the model's model info, and confirm that its fields and expands match the canonical serializer.
2. Open the list, read, and update views. Each loads with no `400`.
3. Create an object. The save redirects to the update view.
4. Save the update view. The save succeeds with no `400`.
5. Send a PUT and a PATCH for one object. Both run the write serializer's validation.
