---
url: "https://vueda.dev/v3/guides/customize-model-info-metadata.md"
title: "Customize Model Info Field and Expand Metadata"
---

# Customize Model Info Field and Expand Metadata

This guide changes the [`model_fields`](/v3/reference/api/rest/schemas/ModelInfoField.md) and [`model_expands`](/v3/reference/api/rest/schemas/ModelInfoExpand.md) entries that [Model Info](/v3/reference/glossary.md#model-info) reports for a serializer. VUEDA generates both from the serializer's declarations. The hooks below let you correct an entry that does not match what the field returns.

The hooks work on the server's wire keys, such as `type_serializer` and `read_only`. For each section's keys and how the client renames them, see [Server-Client Metadata Contract](/v3/core-concepts/server-client-metadata-contract.md).

## When to Override

A [`SerializerMethodField`](https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield) has no model column and no fixed field class to inspect. Its generated entry always has `type_serializer: "SerializerMethodField"`, `type_db` and `type_model` set to `null`, `read_only: true`, and `required: false`.

Override `get_field_model_info` when you want the entry to name the type that `get_<field>()` returns.

The same applies to a field whose `source` names a [`@property`](https://docs.python.org/3/builtins/functions.html#property) or an annotated value instead of a model field. Its `type_db` and `type_model` are `null`, and the [`vueda_info.W001`](/v3/reference/api/py/vueda.info.checks.md#check_field_source_resolution) system check warns about it ([Failure Modes and Recovery](/v3/core-concepts/server-client-metadata-contract.md#failure-modes-and-recovery) gives the full rule). Filling in `type_db` or `type_model` through the hook also clears that warning. The hook never clears a warning about a model's `<field>_lookup_expression`; fix the expression instead.

## Correct a Field Entry

Override [`get_field_model_info`](/v3/reference/api/py/vueda.core.serializers/VuedaExpandableFieldsSerializerMixin.md#get_field_model_info) on the serializer. It receives the generated metadata dict, keyed by field name, and must return a dict in the same shape.

1. Call `super().get_field_model_info(fields)` first. The base implementation applies [`field_display_choices`](/v3/reference/api/py/vueda.core.serializers/VuedaExpandableFieldsSerializerMixin.md#field_display_choices), and [`VuedaSerializer`](/v3/reference/api/py/vueda.core.serializers/VuedaSerializer.md) also fills in its workflow state fields.
2. Change only the keys that are wrong. The generated entry already has `label`, `read_only`, `required`, and `hidden`.
3. Return the dict.

```python
class CustomerSerializer(VuedaSerializer):
    number_of_ordered_products = serializers.SerializerMethodField()

    class Meta(VuedaSerializer.Meta):
        model = Customer
        fields = ["id", "user", "number_of_ordered_products"] + VuedaSerializer.Meta.fields

    def get_number_of_ordered_products(self, obj):
        return obj.orders.count()

    def get_field_model_info(self, fields):
        fields = super().get_field_model_info(fields)
        fields["number_of_ordered_products"]["type_serializer"] = "IntegerField"
        return fields
```

`type_serializer` takes a DRF field class name, such as [`CharField`](https://www.django-rest-framework.org/api-guide/fields/#charfield) or [`IntegerField`](https://www.django-rest-framework.org/api-guide/fields/#integerfield). The [DRF serializer fields reference](https://www.django-rest-framework.org/api-guide/fields/) lists them. The client picks a widget from `type_serializer`, then `type_model`; [Contract-First Dynamic UI](/v3/core-concepts/contract-first-dynamic-ui.md) explains that lookup.

For a real example, [`VuedaSerializer.get_field_model_info`](/v3/reference/api/py/vueda.core.serializers/VuedaSerializer.md#get_field_model_info) sets `type_db` and `type_model` to `CharField` for `workflow_state_code` and `workflow_state_name`. Both fields read through a `@property` on workflow models.

Model info calls the hook on a serializer instance built with the model info request's context. So `self.context["view"]` is the model info endpoint's own view, whose action is `retrieve`. It is not the model's own viewset.

To correct a field inside an [expand](/v3/reference/glossary.md#expand), override `get_field_model_info` on the expanded serializer. VUEDA applies that serializer's own hook when it builds the expand's child fields. For an expand, VUEDA calls the hook on an instance created with no context, so `self.context` is empty there. Write a hook that an expanded serializer uses so that it reads only the `fields` argument.

## Add Display-Only Value Labels

Set `field_display_choices` when a stored value needs a read-only label but should stay an ordinary editable field. For example, a boolean can keep its toggle widget and still show domain labels in read-only views:

```python
class SubmissionSerializer(VuedaSerializer):
    field_display_choices = {
        "submitted": {
            True: "Submitted",
            False: "-",
            None: "Unknown",
        },
    }

    class Meta(VuedaSerializer.Meta):
        model = Submission
        fields = ["id", "submitted"] + VuedaSerializer.Meta.fields
```

The field's entry gains `display_choices`, a list of `{"label": ..., "value": ...}` objects. The client uses these labels only when it shows the field read-only. They do not change serializer validation, model choices, or the editable widget.

If you also override `get_field_model_info`, call `super()` in it. Otherwise the base implementation does not apply the labels.

## Correct an Expand Descriptor

Override [`get_expand_model_info`](/v3/reference/api/py/vueda.core.serializers/VuedaExpandableFieldsSerializerMixin.md#get_expand_model_info) when a `Meta.expandable_fields` entry is a `SerializerMethodField`. It receives the generated list of descriptors, one per entry, and must return a list in the same shape. The default returns the list unchanged, so `super()` is not needed.

A method-field entry has no related serializer, so its generated descriptor has only `name`, `read_only: false`, and `many: false`:

```python
class CustomerSerializer(VuedaSerializer):
    class Meta(VuedaSerializer.Meta):
        model = Customer
        expandable_fields = {
            "recent_orders": serializers.SerializerMethodField,
        }

    def get_recent_orders(self, obj):
        return OrderSerializer(obj.orders.recent(), many=True, context=self.context).data

    def get_expand_model_info(self, expands):
        for expand in expands:
            if expand["name"] == "recent_orders":
                expand["many"] = True
                expand["read_only"] = True
        return expands
```

The client builds an expand's child form fields from the descriptor's `f` key, and relation choices from its `app_label` and `model`. A method-field descriptor has none of these, so the client renders no child fields for it.

An entry backed by a serializer class with a `Meta.model` already gets `app_label`, `model`, and child field metadata under `f`, with no override.

## Check the Result

1. Request the corrected sections as a signed-in user: `GET /routes/vueda.info/model_info/<app_label>/<model>/?e=model_fields,model_expands`. The endpoint is [Get model info](/v3/reference/api/rest/vueda.info/vueda.info_model_info_retrieve.md).
2. Run `python manage.py check` and confirm no `vueda_info.W001` warning remains for the field.

The same hooks feed the OpenAPI schema. The schema lists only field and expand names, as the allowed values of the `f` and `e` [query parameters](/v3/reference/glossary.md#wire-query-parameters). A type correction does not appear there; an entry that a hook adds or removes does.

## Serializers That Do Not Inherit VuedaSerializer

[Registration](/v3/reference/glossary.md#canonical-registration) requires only a `Meta.model` on the [Canonical Serializer](/v3/reference/glossary.md#canonical-serializer). [VuedaExpandableFieldsSerializerMixin](/v3/reference/api/py/vueda.core.serializers/VuedaExpandableFieldsSerializerMixin.md) defines both hooks and the expand descriptor generation, and `VuedaSerializer` includes it.

A plain [`ModelSerializer`](https://www.django-rest-framework.org/api-guide/serializers/#modelserializer) without that mixin still gets generated `model_fields`. Its `model_expands` is an empty list, even when it declares `Meta.expandable_fields`.

To add the hooks and `model_expands`, inherit the mixin together with [`FlexFieldsSerializerMixin`](https://drf-flex-fields2.readthedocs.io/en/stable/api/rest_flex_fields2/serializers/FlexFieldsSerializerMixin.html#rest_flex_fields2.serializers.FlexFieldsSerializerMixin) from `rest_flex_fields2`:

```python
from rest_flex_fields2.serializers import FlexFieldsSerializerMixin
from rest_framework import serializers
from vueda.core.serializers import VuedaExpandableFieldsSerializerMixin


class PlainSerializer(VuedaExpandableFieldsSerializerMixin, FlexFieldsSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = SomeModel
        expandable_fields = {
            "related": (RelatedSerializer, {}),
        }

    def get_field_model_info(self, fields):
        fields = super().get_field_model_info(fields)
        ...
        return fields
```

Keep the mixin first. `FlexFieldsSerializerMixin` serves the `e` query parameter and resolves serializers named by a dotted string in `expandable_fields`. Without it, `model_expands` lists expands that the endpoint cannot return. VUEDA's workflow [`StateSerializer`](/v3/reference/api/py/vueda.workflow.serializers/StateSerializer.md) uses the same three bases.
