Skip to content

Use Expand and Sparse Field Controls ​

This guide declares which relations a model's API can Expand, permits them per action, and matches the client's requests to those permits. It also covers Sparse Fields on expanded objects.

It assumes a working CRUD surface. If the model is not registered and routed yet, start with Create a CRUD Surface. Field and Expand Semantics 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 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's expandable_fields become the expands section of 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 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 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 describes the query plan.

Expand a generic foreign key ​

For a Django: GenericForeignKey, declare GenericForeignKeySerializer 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 or 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.

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:

    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 sends the resolved 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 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 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. When expand names a relation, storeModelConfig adds a 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, 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 describes how unknown query keys are rejected, and Error and Validation Contract 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. Only a create, update, or patch 400 becomes a FormValidationError. 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.

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 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 always adds available_actions to f, and DetailView, ViewRead, and ViewUpdate fetch through it. Code that fetches with defaultObjectRetrieve directly must add it to f.