---
url: "https://vueda.dev/v3/core-concepts/contract-first-dynamic-ui.md"
title: "Contract-First Dynamic UI"
---

# Contract-First Dynamic UI

VUEDA's client ships without knowledge of your models: which exist, what fields they have, or which actions they offer. It builds routes, forms, field and widget components, and action views at runtime from the server's metadata, called [Model Info](/v3/reference/glossary.md#model-info). It reads that metadata through [stores/storeModelInfo](/v3/reference/api/js/stores/storeModelInfo.md).

This page explains how the client turns that metadata into a default UI, and how you customize the parts that need it. It describes configuration precedence (with its exceptions) and field and widget resolution in full. [Server-Client Metadata Contract](/v3/core-concepts/server-client-metadata-contract.md) describes what the metadata contains, and [Canonical Registration and Model Discovery](/v3/core-concepts/canonical-registration-and-discovery.md) describes how a model gets metadata.

## The Default UI and Customization

A model registered with a serializer and a viewset gets a complete [CRUD](/v3/reference/glossary.md#crud) surface with no client code. When a field changes on the server, the form changes with it. When the server stops offering an action, its route goes away.

The default UI is a starting point. A view that needs tweaking, or does not fit the default conventions, takes overrides: for a field, for a widget, or for the whole view. Everything an override leaves alone still follows the metadata. [Configuration Precedence](/v3/core-concepts/contract-first-dynamic-ui.md#configuration-precedence) describes how overrides combine with the defaults.

Model info is the only source the client uses for a model's shape: its fields, [expands](/v3/reference/glossary.md#expand), filters, ordering, and [model-level actions](/v3/reference/glossary.md#model-actions). A user's actions can also come from the [workflow](/v3/reference/glossary.md#workflow) and from [each object's own availability](/v3/reference/glossary.md#available-actions). [Action Contract and Availability](/v3/core-concepts/action-contract-and-availability.md) describes where each action comes from. When the client does something unexpected, start with the metadata it received.

Registration decides what the client can build. A [registered](/v3/reference/glossary.md#canonical-registration) model gets metadata, routes, forms, and permission gating. A model or endpoint outside registration gets none of these, and the client cannot add an action that the server does not offer.

## Where the Metadata Comes From

The server derives model info from the model, [serializer](/v3/reference/glossary.md#canonical-serializer), [viewset](/v3/reference/glossary.md#canonical-viewset), and filterset definitions. These are the same definitions that the server enforces. A field that is read-only in the serializer arrives read-only, and an action that the viewset does not implement does not appear.

## Client Derivation Layer

The client renames model info's keys when it stores them; [Server-Client Metadata Contract](/v3/core-concepts/server-client-metadata-contract.md#client-normalization) lists the names. It then derives each part of the UI from that metadata, with no per-model client code.

### Route Availability

Every model shares the same [CRUD Routes](/v3/reference/glossary.md#crud-routes). [Route Admission](/v3/reference/glossary.md#route-admission) opens a route only for one of the user's [permitted transitions](/v3/reference/glossary.md#permitted-transitions) or an action that the server lists for the user. [Routing and View Resolution Model](/v3/core-concepts/routing-and-view-resolution-model.md) describes the guard chain and what happens when a guard rejects a route.

### Action View Resolution

For each admitted action, [ViewActionRouter](/v3/reference/api/vue/ViewActionRouter.md) picks one view through [Action View Resolution](/v3/reference/glossary.md#action-view-resolution). [setCrudComponents](/v3/reference/api/js/router/routerComponent/functions/setCrudComponents.md) changes the built-in view registry. The order and the transition cases are in [View Component Resolution Order](/v3/core-concepts/routing-and-view-resolution-model.md#view-component-resolution-order).

### Field and Widget Resolution

Each form field renders as a [field component](/v3/reference/glossary.md#form-field) that wraps a [widget component](/v3/reference/glossary.md#widget). The field component handles label, layout, help text, and error display. The widget is the input itself, such as a text box, select, or date picker.

The default field component is [FormField](/v3/reference/api/vue/FormField.md) for every field type. The one type-driven exception is an expand in the form's field list, which renders as an [inline field set](/v3/reference/glossary.md#inline): [FieldSetStackedInline](/v3/reference/api/vue/FieldSetStackedInline.md) for a many relation, [FieldSetSingularStackedInline](/v3/reference/api/vue/FieldSetSingularStackedInline.md) otherwise.

The default widget comes from [type mapping tables](/v3/reference/api/js/utils/fieldMappings.md) keyed on the field's [`typeSerializer`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#typeSerializer), then its [`typeModel`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#typeModel). When `typeModel` is empty, the entry marked [`default`](/v3/reference/api/js/utils/fieldMappings/types/FieldMappingEntry.md#default) for that serializer type applies. [Choice fields](/v3/reference/glossary.md#choice-backed-field) and many-valued fields have their own tables, checked before the base table. [`typeDb`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#typeDb) only selects props for a [`GeneratedField`](https://docs.djangoproject.com/en/6.1/ref/models/fields/#django.db.models.GeneratedField). [mergeDefaultFieldMappings](/v3/reference/api/js/utils/fieldMappings/functions/mergeDefaultFieldMappings.md) adds or replaces entries in the base table at runtime.

A read-only field takes its type's read-only widget. When the type has none, it takes [WidgetReadOnly](/v3/reference/api/vue/WidgetReadOnly.md). An editable field whose type has no mapping has no widget. That field renders an error in place of its input, and the rest of the form still renders. [Custom Field and Widget Rendering](/v3/guides/custom-field-widget-rendering.md) describes that fallback and the steps to add a mapping.

### Form Generation

The form builder turns each field's metadata into a field component, a widget component, props for each, and choice-loading behaviour. Fields with static choices get their options inline in the metadata. Fields marked `choices: true` load their options from the server: a relation's widget searches the related model's list endpoint, and a slug relation's widget fetches from the [choices endpoint](/v3/reference/api/rest/vueda.info/vueda.info_model_info_choices_list.md) when it holds a value or first gets focus. [Choice-Backed Fields and Lookup Models](/v3/guides/choices-and-lookups.md) lists where each kind of field loads its options.

The client flattens expanded relations into [dotted field names](/v3/reference/glossary.md#field-path) (`expandName.fieldName`). An override for a nested field uses the same keys as one for a base field. Composables and symbol-based `provide` and `inject` share [form state](/v3/reference/glossary.md#form-context): values, errors, touched state, and modification tracking. Nested field sets join the same form without passing props down.

## Configuration Precedence

Through two layers above the server's defaults, the client can reshape what it derives. [Model Config](/v3/reference/glossary.md#model-config) is a Pinia store, [stores/storeModelConfig](/v3/reference/api/js/stores/storeModelConfig.md), filled with [`setConfig`](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md). It has a generic layer for all views of a model and a view layer keyed by view name (`list`, `create`, `update`, `read`, and others). For the same key, the view layer wins over the generic layer. Component props are the props that a view or form receives from the template that renders it. [Reactive Data Flow](/v3/core-concepts/reactive-data-flow.md) describes how the store builds and caches each view's config.

### The Rule

For fields and widgets, precedence is: **component props > model config > server-derived defaults.** How a layer wins depends on the kind of key:

- **Component maps** ([OverridingModelConfig.fieldComponents](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#fieldComponents), [OverridingModelConfig.widgetComponents](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#widgetComponents)): the first layer with an entry for the field wins. An entry can be a component, the name of a registered component, or a function that returns a component. An unknown name raises an error for that field.
- **Prop maps** ([OverridingModelConfig.fieldProps](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#fieldProps), [OverridingModelConfig.widgetProps](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#widgetProps)): the layers merge key by key. Defaults come first (the type mapping's props, plus the server's field entry for field props), then model config, then component props.
- **Field lists** ([`fields`](/v3/reference/api/vue/FormModel.md#prop-fields), [OverridingModelConfig.expand](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#expand), [`computedFields`](/v3/reference/api/vue/FormModel.md#prop-computedFields)): a component prop replaces the model config list whole.
- **Field metadata** ([OverridingModelConfig.fieldDetails](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#fieldDetails)): entries merge over the server's field entries, per property, in the same order.

The resolver sets a few props after every layer, so no layer can override them. On the widget these are [`readOnly`](/v3/reference/api/js/use/useWidget.md#WIDGET_PROPS) and the choice source (`options`, or the endpoint coordinates for `choices: true`). On the field they are [`name`](/v3/reference/api/vue/FormField.md#prop-name) and [`readOnly`](/v3/reference/api/vue/FormField.md#prop-readOnly).

### Exceptions

Three cases do not follow the rule:

- **Read-only fields take the read-only widget.** When a field renders read-only, the resolver ignores `widgetComponents` from both layers and renders the type's read-only widget or `WidgetReadOnly`. A field renders read-only when the server marks it read-only, when the view is `read`, when it is a computed field, or when either layer sets `readOnly: true`. To customize it, replace the field component with `fieldComponents`, fill the field's [`widget(<name>)`](/v3/reference/api/vue/FieldRenderer/slots.md#slot-widget-fieldName) slot, or make the field editable.
- **Computed fields always use `FormField`.** A [computed field](/v3/reference/glossary.md#computed-field) ignores `fieldComponents` from both layers.
- **`readOnly: false` wins from either layer.** An explicit `readOnly: false` in the `fieldProps` of either layer makes the field editable in the UI. It beats a server read-only marker, a `read` view, and a `readOnly: true` in the other layer, so a model config `false` beats a component prop `true`.

[`formProps`](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#formProps) in model config are extra props for the form model. Every form view applies three layers in order. Model config `formProps` comes first, then the view's own [`formProps`](/v3/reference/api/vue/ViewCreate.md#prop-formProps) prop. The view's form props, such as `fieldProps`, `widgetProps`, and `formModelVariant`, come last. A form prop that the template does not pass leaves the configured value in place. A later layer replaces a value whole, so a `fieldProps` map passed to the view replaces the configured map instead of merging with it.

The view always sets `app`, `model`, and `view`, because they select the configuration that the form reads. The view ignores those keys in a `formProps` layer and logs a console warning. The read view, [ViewRead](/v3/reference/api/vue/ViewRead.md), takes no form props, so its form receives model config `formProps` alone. A custom view shell gets the same order from [use/useFormModelProps](/v3/reference/api/js/use/useFormModelProps.md).

List columns resolve through their own chain, described in [Customize List Column Rendering](/v3/guides/customize-list-column-rendering.md).

### UI Overrides and Server Enforcement

Overrides change only what the client renders. Field props can replace `required`, `label`, or `readOnly` for the UI, and `fieldDetails` can replace any property of a field's metadata. The server applies its own validation and read-only rules on every write, whatever the form sent.

Overrides cannot add actions. The route guard admits only workflow transition codes and actions that the server lists, so an action name that only model config knows gets no route and no view. [`routeActions`](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#routeActions) can only narrow the server's list. Hiding an action in the client does not remove its endpoint; [Authorization vs UI Semantics](/v3/core-concepts/authorization-vs-ui-semantics.md) describes that boundary.

## Resolution Determinism

The same metadata, configuration, and registered mappings always produce the same UI. The type mapping tables and the view registry are module-level state, so `mergeDefaultFieldMappings` and `setCrudComponents` change the result for every model at once. Precedence has fixed layers, the resolver applies the three exceptions the same way in every form, and view resolution ends at exactly one component.

This is what makes a UI problem traceable. Debugging it means inspecting the metadata that the client received and the configuration it applied.

## Failure Modes

**Client code names a field that the server no longer sends.** A configured field list that names a field missing from the metadata throws `Unknown field ... specified` when the form builds. Project overrides and custom views reintroduce this risk whenever they reference contract details by name.

**Premature overrides hide defaults.** The default mapping is often correct. An override added before you understand the default adds maintenance and becomes one more variable when something breaks.

**Client-side restriction mistaken for server-side removal.** Removing an action from `routeActions` hides it in the client, but the endpoint still accepts requests from any user the server permits.

**Dynamic choices assumed to be loaded.** A field with `choices: true` has no options until its widget fetches them. Code that reads its options early sees an empty list, and keeps seeing one if the fetch fails.

**Widget overrides on read-only fields.** A `widgetComponents` entry for a field that renders read-only has no effect, as described under [Exceptions](/v3/core-concepts/contract-first-dynamic-ui.md#exceptions).

**Nested object paths for expanded fields.** Overrides for expanded fields use the flattened dotted key. A nested object path does not match any field, and the override is ignored.

**A failed model-info fetch blocks the model.** The client caches the failure, so the model stays unavailable until the cache clears. [Reactive Data Flow](/v3/core-concepts/reactive-data-flow.md) describes when cached errors clear.
