---
url: "https://vueda.dev/v3/guides/hide-fields-per-write-action.md"
title: "Lock Fields to Specific Write Actions"
---

# Lock Fields to Specific Write Actions

[ExcludeFieldsSerializerMixin](/v3/reference/api/py/vueda.core.serializers/ExcludeFieldsSerializerMixin.md) makes named fields read-only for `create`, or for `update` and `partial_update`, on one serializer. Use it when the read and write field sets differ by only a few fields. For example, a timesheet's `supervisor` is set only after creation, and its `employee` cannot change once the timesheet exists.

When the write field set differs enough that you want separate serializer classes, follow [Split Read/Write Serializers Safely](/v3/guides/split-read-write-serializers.md) instead.

## Declare the Exclusions

Put the mixin before [VuedaSerializer](/v3/reference/api/py/vueda.core.serializers/VuedaSerializer.md) in the class bases. List field names in `Meta.exclude_create_fields` and `Meta.exclude_update_fields`:

```python
from vueda.core.serializers import ExcludeFieldsSerializerMixin
from vueda.core.serializers import VuedaSerializer


class TimesheetSerializer(ExcludeFieldsSerializerMixin, VuedaSerializer):
    class Meta(VuedaSerializer.Meta):
        model = Timesheet
        fields = ["id", "period_start", "period_end", "employee", "supervisor"] + VuedaSerializer.Meta.fields
        exclude_create_fields = ["supervisor"]
        exclude_update_fields = ["employee"]
```

The mixin marks each `exclude_create_fields` entry read-only when the view's action is `create`. It marks each `exclude_update_fields` entry read-only when the action is `update` or `partial_update`, so one list covers `PUT` and `PATCH`. Any other action, including an [Extra Action](/v3/reference/glossary.md#extra-action), gets no exclusions.

The mixin only adds read-only markings. A field that is read-only through `read_only_fields` or `extra_kwargs` stays read-only for every action. Excluded fields stay in `Meta.fields` and in every response.

The markings go through DRF's `extra_kwargs`, which reach only the fields that the serializer builds from the model. A field declared on the serializer class ignores both lists and stays writable. List only generated fields.

Exclude only fields that the database can leave empty: nullable columns, or columns with a default. A `create` that leaves a required column unset raises [Django: IntegrityError](https://docs.djangoproject.com/en/6.1/ref/exceptions/#django.db.IntegrityError) on insert, and the client gets a `500`.

Use a serializer with this mixin only on a ViewSet, because only a ViewSet has an action. Any other DRF view is rejected by a system check and at request time. [Attach the Serializer to Its Viewset](/v3/guides/hide-fields-per-write-action.md#attach-the-serializer-to-its-viewset) explains why.

## Attach the Serializer to Its Viewset

The mixin reads the action from the view in the serializer context. Make the serializer the [DRF: GenericAPIView.serializer\_class](https://www.django-rest-framework.org/api-guide/generic-views/#attributes) of the routed viewset that handles the request:

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


class TimesheetViewSet(VuedaViewSet):
    queryset = Timesheet.objects.all()
    serializer_class = TimesheetSerializer
```

Registering the pair with [info.registration.register](/v3/reference/api/py/vueda.info.registration.md#register) works the same way. That is the [Canonical Registration](/v3/reference/glossary.md#canonical-registration) that most models use.

When the context has no view, the mixin excludes no fields. Every field keeps its declared writability. This covers code that builds the serializer without a context.

A [Serializer-Only Registration](/v3/reference/glossary.md#serializer-only-registration) made with [info.registration.register\_serializer](/v3/reference/api/py/vueda.info.registration.md#register_serializer) is also allowed. The serializer has no ViewSet of its own, so it receives no `create` or `update` request for the exclusions to restrict. Model info builds it either without a view or with the model info endpoint's own ViewSet in context. That ViewSet's `retrieve` action matches neither exclusion list, so neither way locks a field.

### Views That Are Not ViewSets

Only a ViewSet has an action. A DRF generic view, such as `CreateAPIView` or `UpdateAPIView`, handles `create` and `update` requests without one, so the mixin cannot tell which exclusions apply. A write through such a view could set every field that the exclusions lock. So VUEDA does not allow the mixin on any view that is not a ViewSet:

- The mixin raises `ImproperlyConfigured` when it builds its fields with such a view in context. That happens on every request that builds the serializer, reads included. A request that never builds it, such as a `DELETE`, does not raise.
- The `vueda_core.E021` system check reports a routed view whose serializer uses the mixin, before any request reaches it. The error names each route of that view. [Run the System Check](/v3/guides/hide-fields-per-write-action.md#run-the-system-check) lists the uses that the check cannot see.

The rule also covers read-only views such as `RetrieveAPIView`. They cannot write, but one rule for every view that is not a ViewSet is simpler to follow than a rule that depends on which methods a view handles. For a generic view, use a serializer without the mixin. To lock a field there, pass `read_only=True` to the field, or give that view its own serializer class that lists the field in `Meta.read_only_fields`.

VUEDA's own `WhoIsView` is a `RetrieveAPIView`. It uses the serializer named by `REST_AUTH["USER_DETAILS_SERIALIZER"]`. If `E021` names `WhoIsView`, point that setting at a serializer without the mixin.

### Nested and Expanded Serializers

Do not nest this serializer as a declared field on another serializer, and do not name it in another serializer's `Meta.expandable_fields`. A nested or expanded serializer shares its parent's context, so its exclusions would follow the parent view's action rather than an action of its own. VUEDA does not support that behavior, and the system check rejects both placements. Put the exclusions on the routed serializer, and nest or expand a serializer without the mixin.

The parent's own exclusions cannot lock fields inside a nested serializer either. A nested serializer is a declared field, and declared fields ignore both lists. To lock a nested serializer's fields for one action, give that action its own serializer classes. Subclass the nested serializer and mark the fields read-only in its `Meta`. Then subclass the parent to nest that version, and select the parent subclass for the action with [PerActionSerializerMixin](/v3/reference/api/py/vueda.core.viewsets/PerActionSerializerMixin.md):

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


class InvoiceLineSerializer(VuedaSerializer):
    class Meta(VuedaSerializer.Meta):
        model = InvoiceLine
        fields = ["id", "product", "quantity"] + VuedaSerializer.Meta.fields


class InvoiceLineUpdateSerializer(InvoiceLineSerializer):
    class Meta(InvoiceLineSerializer.Meta):
        read_only_fields = ["product"]


class InvoiceSerializer(VuedaSerializer):
    lines = InvoiceLineSerializer(many=True)

    class Meta(VuedaSerializer.Meta):
        model = Invoice
        fields = ["id", "customer", "lines"] + VuedaSerializer.Meta.fields


class InvoiceUpdateSerializer(InvoiceSerializer):
    lines = InvoiceLineUpdateSerializer(many=True)


class InvoiceViewSet(PerActionSerializerMixin, VuedaViewSet):
    queryset = Invoice.objects.all()
    serializer_class = InvoiceSerializer
    update_serializer_class = InvoiceUpdateSerializer
    partial_update_serializer_class = InvoiceUpdateSerializer
```

A `create` request can set each line's `product`. An `update` or `partial_update` request can change a line's `quantity` but not its `product`.

The read-only `product` also applies to a line that an update request adds. That line is saved without a `product`, which fails when the foreign key is required. If an update request can add lines, keep `product` writable and reject a changed `product` on an existing line in `validate()` instead. [Split Read/Write Serializers Safely](/v3/guides/split-read-write-serializers.md) covers per-action serializer classes in more detail.

## Run the System Check

Run `manage.py check` after you add the mixin or move a serializer that uses it. [core.checks.check\_exclude\_fields\_serializer\_usage](/v3/reference/api/py/vueda.core.checks.md#check_exclude_fields_serializer_usage) starts from every routed DRF view and every registered serializer. From each serializer, it follows nested fields and `Meta.expandable_fields`. It reports:

- `vueda_core.E007` for a serializer with the mixin used as a nested field.
- `vueda_core.E008` for a serializer with the mixin named in any `Meta.expandable_fields` entry, even when it is also routed.
- `vueda_core.E021` for a routed DRF view that is not a ViewSet and whose serializer uses the mixin.

Each error names the serializer. `E007` and `E008` also name the parent serializer and field, and `E021` names the view and each of its routes.

`E021` reads the `serializer_class` passed to `as_view()`. When none was passed, it reads the view's `serializer_class` attribute. It cannot see a serializer that a view picks in `get_serializer_class()`.

It also cannot see a serializer that another serializer builds while it renders a response. A [GenericForeignKeySerializer](/v3/reference/api/py/vueda.core.serializers/GenericForeignKeySerializer.md) field builds the [Canonical Serializer](/v3/reference/glossary.md#canonical-serializer) of the object that it points to, with the parent's context. When that canonical serializer uses the mixin and the parent's view is not a ViewSet, the response fails with `ImproperlyConfigured`.

In both cases, the mixin's `ImproperlyConfigured` error is the only report, and it comes when a request builds that serializer.

## Reject Submitted Values Explicitly (Optional)

A request that sends a value for an excluded field still succeeds. DRF skips read-only fields when it reads the body, so no `400` is returned:

- On `create`, the new row stores the field's model default, for example `None` for a nullable foreign key.
- On `update` and `partial_update`, the field keeps its stored value.

To answer with a `400` instead, check the raw body in `validate()` and raise [VuedaValidationError](/v3/reference/api/py/vueda.core.exceptions/VuedaValidationError.md):

```python
from vueda.core.exceptions import VuedaValidationError


class TimesheetSerializer(ExcludeFieldsSerializerMixin, VuedaSerializer):
    # Meta as above

    def validate(self, attrs):
        attrs = super().validate(attrs)
        if self.context["view"].action == "create" and "supervisor" in self.initial_data:
            raise VuedaValidationError({"supervisor": ["Set the supervisor after the timesheet exists."]})
        return attrs
```

`validate()` runs only after every field passes field validation, so a request with other field errors reports those first.

## Hide the Field in Client Forms (Optional)

The exclusions do not change [Model Info](/v3/reference/glossary.md#model-info). Model info describes the [Canonical Serializer](/v3/reference/glossary.md#canonical-serializer) through its own endpoint's view, whose action matches neither list, so model info reports excluded fields as writable. [Canonical Registration and Model Discovery](/v3/core-concepts/canonical-registration-and-discovery.md#how-model-info-uses-the-registry) describes how model info reads the registered serializer.

The default create and update forms therefore render and submit excluded fields, and the server drops the values. To leave a field out of one form, set that view's `displayFields` and `submitFields` with [setConfig](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md):

```js
storeModelConfig().setConfig({ app: "timesheet", model: "timesheet" }, null, {
    create: {
        displayFields: ["period_start", "period_end", "employee"],
        submitFields: ["period_start", "period_end", "employee"],
    },
    update: {
        displayFields: ["period_start", "period_end", "supervisor"],
        submitFields: ["period_start", "period_end", "supervisor"],
    },
});
```

[Configure `list`/`read`/`create`/`update` Views](/v3/guides/configure-crud-views.md#choose-each-views-field-lists) describes the [View Field Lists](/v3/reference/glossary.md#view-field-lists) and how each view uses them.

## Test Checklist

- A `create` that sends a value for each `exclude_create_fields` entry succeeds and saves the model default.
- An `update` and a `partial_update` that send a value for each `exclude_update_fields` entry succeed and keep the stored value.
- `manage.py check` reports no `vueda_core.E007`, `E008`, or `E021` errors.
- `manage.py spectacular`, or your schema generation step, succeeds.

## Troubleshooting

**`manage.py check` reports `vueda_core.E007` or `vueda_core.E008`.** The serializer is nested or expanded somewhere. The error names the parent serializer and field. Remove the mixin from the nested serializer. To lock its fields for one action, follow the example in [Nested and Expanded Serializers](/v3/guides/hide-fields-per-write-action.md#nested-and-expanded-serializers).

**`manage.py check` reports `vueda_core.E021`, or a request raises `ImproperlyConfigured`.** A DRF view that is not a ViewSet uses a serializer with the mixin. The view has no action, so the mixin cannot tell a `create` from an `update`, and a write could set the fields that the exclusions lock. Route the serializer through a ViewSet, or follow [Views That Are Not ViewSets](/v3/guides/hide-fields-per-write-action.md#views-that-are-not-viewsets) for other ways to lock the fields.

**An excluded field is writable outside a request.** This is expected. Without a view in context, the mixin excludes no fields. The exclusions apply only to `create`, `update`, and `partial_update` requests handled by a ViewSet.

**`IntegrityError` on create.** A field in `exclude_create_fields` has a required database column. Give the column a default, make it nullable, or remove the field from the list.

**The OpenAPI schema marks the wrong fields read-only.** drf-spectacular describes the `create`, `update`, and `partial_update` request bodies with one shared component named after the serializer. Its read-only markings match only one of those actions.
