---
url: "https://vueda.dev/v3/guides/expand-and-fields-controls.md"
title: "Use Expand and Sparse Field Controls"
---

# Use Expand and Sparse Field Controls

This guide declares which relations a model's API can [Expand](/v3/reference/glossary.md#expand), permits them per action, and matches the client's requests to those permits. It also covers [Sparse Fields](/v3/reference/glossary.md#sparse-fields) on expanded objects.

It assumes a working [CRUD](/v3/reference/glossary.md#crud) surface. If the model is not registered and routed yet, start with [Create a CRUD Surface](/v3/guides/create-crud-surface.md). [Field and Expand Semantics](/v3/core-concepts/field-and-expand-semantics.md) describes the rules behind these steps, including how the client derives its default field lists and what each view sends in `f` and `e`.

## Declare expandable fields

In the serializer's `Meta.expandable_fields`, list each relation that the client may expand. Merge the parent's entries at the end, so expands that [VuedaSerializer](/v3/reference/api/py/vueda.core.serializers/VuedaSerializer.md) declares are kept:

```python
from vueda.core.serializers import VuedaSerializer
from .models import Widget
from .serializers import CategorySerializer, TagSerializer

class WidgetSerializer(VuedaSerializer):
    class Meta(VuedaSerializer.Meta):
        model = Widget
        fields = [
            "id", "name", "status", "category", "tags",
        ] + VuedaSerializer.Meta.fields

        expandable_fields = {
            "category": (CategorySerializer, {}),
            "tags": (TagSerializer, {"many": True}),
        }
        expandable_fields.update(VuedaSerializer.Meta.expandable_fields)
```

The [Canonical Serializer](/v3/reference/glossary.md#canonical-serializer)'s `expandable_fields` become the expands section of [Model Info](/v3/reference/glossary.md#model-info). The client reads each expand's details from there, and requests an expand only when a view's model config names it.

Each entry takes one of three forms:

- A tuple of `(SerializerClass, options)`. The options dict holds [`rest_flex_fields2`](https://drf-flex-fields2.readthedocs.io/en/stable/api/rest_flex_fields2/serializers/FlexFieldsSerializerMixin.html#rest_flex_fields2.serializers.FlexFieldsSerializerMixin) options, such as `"many": True` for a to-many relation.
- A bare serializer class, when the expand needs no options.
- A dotted import path string, alone or as the tuple's first element. It is resolved the first time the field expands, which avoids circular imports between serializer modules.

```python
expandable_fields = {
    "category": CategorySerializer,
    "tags": ("myapp.serializers.TagSerializer", {"many": True}),
}
```

The value must be a tuple. A list such as `[TagSerializer, {}]` fails the system check `vueda_core.E001`. The check [`check_expandable_fields_configuration`](/v3/reference/api/py/vueda.core.checks.md#check_expandable_fields_configuration) runs with Django's system checks, for example on `manage.py check`. It covers each routed viewset's `serializer_class`, each registered serializer, and every serializer they reach through nested fields and `expandable_fields`.

Do not name an expand `related` or `calculated`. The client reads a path that starts with `related.` or `calculated.` from the row's related or calculated object. The fields of an expand with either name would render blank. The system check `vueda_core.E022` reports such an expand. Give the expand another name, and set `source` in its options to read the model field:

```python
expandable_fields = {
    "related_widget": (WidgetSerializer, {"source": "related"}),
}
```

The same holds for a dotted path through a serializer field named `related` or `calculated`, such as a JSON field. Building the model config throws when `displayFields`, `fetchFields`, or `submitFields` names such a path. A field of either name still works as a single segment, such as `related` in `displayFields`.

Each expanded relation adds one join or prefetch query per request and adds its data to every row. [Field and Expand Semantics](/v3/core-concepts/field-and-expand-semantics.md) describes the query plan.

## Expand a generic foreign key

For a [Django: GenericForeignKey](https://docs.djangoproject.com/en/6.1/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey), declare [GenericForeignKeySerializer](/v3/reference/api/py/vueda.core.serializers/GenericForeignKeySerializer.md) as the expand. The key must be the `GenericForeignKey` field's name on the model:

```python
from django.conf import settings
from vueda.core.serializers import GenericForeignKeySerializer, VuedaSerializer

class NoteSerializer(VuedaSerializer):
    class Meta(VuedaSerializer.Meta):
        model = Note
        fields = ["id", "content_type", "object_id", "text"] + VuedaSerializer.Meta.fields

        expandable_fields = {
            "content_object": (
                GenericForeignKeySerializer,
                {settings.REST_FLEX_FIELDS2["FIELDS_PARAM"]: ["*"]},
            ),
        }
        expandable_fields.update(VuedaSerializer.Meta.expandable_fields)
```

`"*"` in the fields option returns every field of the related object's serializer. Without static field options, declare the bare class: `"content_object": GenericForeignKeySerializer`.

At representation time, `GenericForeignKeySerializer` serializes the related object with the serializer registered for its model. Register every model that the relation can point to, with [`register`](/v3/reference/api/py/vueda.info.registration.md#register) or [`register_serializer`](/v3/reference/api/py/vueda.info.registration.md#register_serializer). An object whose model is not registered expands to `null`. The output always carries `app_label`, `model`, and `formatted_name`.

A generic foreign key expand is read-only. The server does not check `f` entries under it, such as `content_object.name`, because the related model is known only per object.

### Filter fields per related model

To apply a field filter to one related model only, write a model-targeted specifier in the fields or omit option:

```text
_<app_label>__<model_name>__<field_name>
```

The leading `_` marks the specifier, and double underscores separate its three parts. Single underscores inside a part are allowed. `<model_name>` is the lowercase class name, as in `instance._meta.model_name` (`PackingBox` becomes `packingbox`).

```python
expandable_fields = {
    "content_object": (
        GenericForeignKeySerializer,
        {
            settings.REST_FLEX_FIELDS2["FIELDS_PARAM"]: ["*"],
            settings.REST_FLEX_FIELDS2["OMIT_PARAM"]: [
                # Removed from every related model.
                "available_actions",
                # Removed only when the related object is a Distributor.
                "_store__distributor__description",
                # Removed only when the related object is a PackingBox.
                "_store__packingbox__carrying_weight",
                "_store__packingbox__depth",
            ],
        },
    ),
}
```

Plain names and wildcards apply to every related model. For each object, the serializer turns the specifiers that name the object's model into plain field names and drops the other specifiers.

Field selection stays within the registered serializer's `Meta.fields`. A model field that the registered serializer does not list is never returned, whether a plain name or a specifier requests it.

## Permit expands on `list`

A `list` request may expand only the names in the viewset's `permit_list_expands`, which is empty until you set it. The client's default `expand` is empty, so a list sends no `e` until its config names an expand. To expand a relation on a list, permit it on the server and name it in the client's config. A list whose `expand` names a relation that the permit list lacks fails with a `400`. While the permit list is empty, each rejected name gets the message `Invalid expands. No expands are permitted.` Once it names expands, the message lists them.

1. Set `permit_list_expands` on the viewset to the expands that the list may use:

    ```python
    from vueda.core.viewsets import VuedaViewSet
    from .models import Widget
    from .serializers import WidgetSerializer

    class WidgetViewSet(VuedaViewSet):
        queryset = Widget.objects.all()
        serializer_class = WidgetSerializer
        permit_list_expands = ["category"]
    ```

2. Name the expands that each view should request. A list view may name only expands from the permit list. Pass the override as the per-view layer of [`setConfig`](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md):

    ```js
    import { storeModelConfig } from "@vueda/stores/storeModelConfig.js";

    storeModelConfig().setConfig(
        { app: "myapp", model: "widget" },
        // Every view expands both.
        { expand: ["category", "tags"] },
        // The list view expands only category.
        { list: { expand: ["category"] } },
    );
    ```

    [ViewList](/v3/reference/api/vue/ViewList.md) sends the resolved [`expand`](/v3/reference/api/js/stores/storeModelConfig/types/ModelConfig.md#expand) as `e`. An empty `expand` sends no `e`, so the list expands nothing and needs no permit.

With a permit list set, a request for an expand that the permit list does not name gets a `400` whose message lists the permitted expands. The `*` wildcard expands every permitted name.

## Narrow expands on other actions

Every other action allows every declared expand until the viewset sets `permit_<action>_expands` for it. [`FlexFieldsMixin`](/v3/reference/api/py/vueda.core.viewsets/FlexFieldsMixin.md) reads the attribute for the current action, so `permit_retrieve_expands` narrows detail requests:

```python
class WidgetViewSet(VuedaViewSet):
    queryset = Widget.objects.all()
    serializer_class = WidgetSerializer
    permit_list_expands = ["category"]
    permit_retrieve_expands = ["category", "tags"]
```

An empty list permits no expands on that action. The `read` and `update` views fetch their object with `retrieve`, so match their `expand` to `permit_retrieve_expands` the same way as the `list` view. [Action-Scoped Expand](/v3/reference/glossary.md#action-scoped-expand) names this per-action restriction.

## Permit nested expands

A dotted path in `e`, such as `customer.user`, expands `customer` and then its `user`. When the action has a permit list, list each dotted path exactly as the client sends it:

```python
class CartViewSet(VuedaViewSet):
    ...
    permit_list_expands = ["cart_items", "customer", "customer.user"]
```

A dotted path that the permit list does not name gets a `400`, even when its first part is permitted. A dotted entry does not permit its first part alone: `e=customer` still needs `"customer"` in the list. An action with no permit list accepts any dotted path that follows declared expands, up to the depth that `MAXIMUM_EXPANSION_DEPTH` sets (4 by default).

## Select fields of an expanded object

To narrow an expanded object, name its fields in `f` as `<expand>.<field>`, as in `f=id,category.id,category.name`. Include the related primary key if you need it, because VUEDA adds no field that you did not name.

In the client, put the same dotted names in the view's [`fetchFields`](/v3/reference/api/js/stores/storeModelConfig/types/ModelConfig.md#fetchFields). When `expand` names a relation, [`storeModelConfig`](/v3/reference/api/js/stores/storeModelConfig/functions/storeModelConfig.md) adds a [`fieldDetails`](/v3/reference/api/js/stores/storeModelConfig/types/ModelConfig.md#fieldDetails) entry for each of its sub-fields, keyed by the dotted name, such as `category.name`. Name those keys in `displayFields` to show sub-field columns.

An expanded object never carries [Available Actions](/v3/reference/glossary.md#available-actions), and `f=category.available_actions` gets a `400`. Retrieve the related object to read its actions.

## Check the requests

`list`, `retrieve`, `create`, `update`, and `partial_update` requests reject an `f` or `e` name that the endpoint does not offer. Each invalid name is a key in the `400` body, and its message lists the valid names. The body has the standard validation shape: each key maps to a list of message strings. [Filtering and Ordering Semantics](/v3/core-concepts/filtering-and-ordering-semantics.md#query-namespace-and-validation-boundary) describes how unknown query keys are rejected, and [Error and Validation Contract](/v3/core-concepts/error-and-validation-contract.md) describes error bodies.

A write checks the names before it validates the body, so a write that gets this `400` saves nothing.

Confirm the setup in the browser's network panel:

- The `list` request's `e` holds only names in `permit_list_expands`, and the response holds expanded objects for them.
- The detail request's `e` holds only names that its action permits.
- Adding an undeclared name to `e` or `f` returns a `400` that names it.
- A per-view `expand` override changes `e` for that view only.

## Troubleshooting

**Every `list` request returns `400` with "Invalid expands".** The client's list `expand` includes a name that `permit_list_expands` does not. The permit list starts empty. Add the name to `permit_list_expands`, or remove it from the list view's `expand`.

**A `400` from `f` or `e` does not reach a form.** A list or detail view that fails its request raises a [FetchError](/v3/reference/api/js/utils/errors/members/FetchError.md). Only a create, update, or patch `400` becomes a [FormValidationError](/v3/reference/api/js/utils/errors/members/FormValidationError.md). Fix the view's `expand` or `fetchFields`, or the viewset's permit list.

**Expanded field columns show no values.** `displayFields` names an `expand.subfield` key, but the view's `expand` does not name that relation. Without the expand, the client builds no `fieldDetails` entry for the key, and the server returns the related primary key. Add the relation to `expand`.

**Building the model config throws `Model config for <app>.<model> is invalid`.** The message names each problem. A dotted entry such as `category.name` in `displayFields`, `fetchFields`, or `submitFields` needs `category` in the same view's `expand`. An `expand` entry must name an expand that model info declares.

**`manage.py check` reports `vueda_core.E022`, or the model config throws for a reserved name.** The serializer names an expand or field `related` or `calculated`. Rename it, as in [Declare expandable fields](/v3/guides/expand-and-fields-controls.md#declare-expandable-fields).

**A form shows a select or a read-only list where an inline fieldset was expected.** A form renders a relation as an inline fieldset only when the view's `expand` names it. Without the expand, a foreign key or a many-to-many relation shows a select, and a reverse foreign key shows its rows read-only. Add the relation to that view's `expand`. [Render a Relation as an Inline](/v3/guides/render-relations-as-inlines.md) describes the rest of the inline setup.

**A write with a nested object returns a type error on the relation.** The request's `e` omits the relation, so the server reads the relation as a primary key. Add the relation to `e`, and the nested object is accepted. The server answers a misspelled or unpermitted `e` name with a `400` that names it, before it validates the body.

**A custom detail component has no action availability.** [useDetailView](/v3/reference/api/js/use/useDetailView/functions/useDetailView.md) always adds `available_actions` to `f`, and [DetailView](/v3/reference/api/vue/DetailView.md), `ViewRead`, and `ViewUpdate` fetch through it. Code that fetches with [defaultObjectRetrieve](/v3/reference/api/js/utils/objectCrud/functions/defaultObjectRetrieve.md) directly must add it to `f`.
