---
url: "https://vueda.dev/v3/core-concepts/pk-and-identifier-discipline.md"
title: "Primary Key and Identifier Discipline"
---

# Primary Key and Identifier Discipline

A model's primary key can be `id`, a slug, a UUID, or several columns. VUEDA reads the primary key field's name from [Model Info](/v3/reference/glossary.md#model-info), so no layer assumes `id`. This page describes how the server marks that field, how identifiers travel between the client and the server, and why choice values are always strings.

## PK Authority and Metadata Source

The server decides which field is the primary key. [`get_model_fields_data`](/v3/reference/api/py/vueda.info.serializers/ModelInfoSerializer.md#get_model_fields_data) compares each serializer field name with the model's primary key name, `Meta.model._meta.pk.name`. The matching field gets the [Pk Marker](/v3/reference/glossary.md#pk-marker), `pk: true`, in `model_fields`. Other fields carry no `pk` key.

The match is by name only. On a model whose primary key is `slug`, the serializer field `slug` gets the marker, and a field named `id` does not. The marker is absent when the serializer's `Meta.fields` leaves out the primary key or exposes it under another name.

The client reads the marker when it normalizes model info. [Server-Client Metadata Contract](/v3/core-concepts/server-client-metadata-contract.md#client-normalization) describes that step, which sets `pk` on the client's model info to the marked field's name. Without a marker, [storeModelInfo](/v3/reference/api/js/stores/storeModelInfo/functions/storeModelInfo.md) throws `no pk field found` and caches the error. [Reactive Data Flow](/v3/core-concepts/reactive-data-flow.md#cached-failures) describes that cached failure and when it clears.

## How the Client Uses the Pk Field Name

The client never tells the server which field is the primary key. It uses the name from model info wherever it needs an object's identifier:

- The default field lists that `storeModelConfig` builds leave out the pk field.
- The built-in views pass the name as `pkKey` to the [CRUD Adapter](/v3/reference/glossary.md#crud-adapter) functions. The create, detail, and update views also add the pk field to the fields that they request.
- [`defaultObjectUpdate`](/v3/reference/api/js/utils/objectCrud/functions/defaultObjectUpdate.md) reads the object's pk from [`pkKey`](/v3/reference/api/js/utils/objectCrud/functions/defaultObjectUpdate.md#param-args-pkKey), which defaults to `"id"`. Code that calls it directly for a model with another pk name, and omits `pkKey`, sends `undefined` as the pk segment.

## Identifier Transport

Identifiers travel in three shapes. Each uses the fixed name `pk` or `pks`, whatever the model's pk field is called.

**One object: a route parameter.** The detail route of the [CRUD Routes](/v3/reference/glossary.md#crud-routes), `/:app/:model/:action/:pk`, carries the object's pk as `params.pk`. The CRUD adapters place that value in the pk segment of the [Model API Path](/v3/reference/glossary.md#model-api-path), which [`getDetailUrl`](/v3/reference/api/js/utils/urls/functions/getDetailUrl.md) builds. A [Composite Primary Key](/v3/reference/glossary.md#composite-primary-key) travels as its JSON array string, such as `["1","42"]`. [Set Up CRUD for a Composite Primary Key Model](/v3/guides/composite-primary-keys.md) describes that format.

**Several objects: a query value.** When [`getCRUDForTo`](/v3/reference/api/js/router/getCrud/functions/getCRUDForTo.md) gets an array as `pk`, it targets the list route with one `pk` query value for each key, as in `?pk=4&pk=7`. An empty array targets the list route with no `pk` value. The list route that [`makeCRUDRoutes`](/v3/reference/api/js/router/makeCrud/functions/makeCRUDRoutes.md) registers passes the values to the view as an array. A list view's selection actions use this route.

Neither function splits a value, so a key that contains a comma reaches the view intact. Every composite key contains a comma, and a free-text key can. A link such as `?pk=4,7` therefore selects the single key `4,7`. The list route ignores empty `pk` values and a bare `?pk`. `getCRUDForTo` throws when its `query` argument has a `pk` entry. Code that passes another route's query to `getCRUDForTo` must remove `pk` first.

**Bulk requests: a body key.** [`defaultObjectsDelete`](/v3/reference/api/js/utils/listCrud/functions/defaultObjectsDelete.md) sends `DELETE` to the model's list path with the body `{ "pks": [...] }`. [`defaultListExecuteAction`](/v3/reference/api/js/utils/listCrud/functions/defaultListExecuteAction.md) sends the same `pks` body for a [Bulk Action](/v3/reference/glossary.md#bulk-action).

The server's bulk delete, activate, and deactivate handlers read the keys with [`PrimaryKeyListSerializer`](/v3/reference/api/py/vueda.core.serializers/PrimaryKeyListSerializer.md). It converts each entry through the model's primary key field. An integer, UUID, or string key travels as its JSON value. A [Composite Primary Key](/v3/reference/glossary.md#composite-primary-key) travels as its JSON array string, the same form as the route parameter. An entry that does not convert answers `400` under `pks`. A key that names no row answers `400` with `Object with pk=... does not exist.` under the key as sent. A key that names a row that the user may not change answers the same way. The response therefore does not reveal that the row exists.

The lookup cache in [useLookupContext](/v3/reference/api/js/use/useLookupContext/functions/useLookupContext.md) converts each pk to a string before using it as a cache key. The number `42` and the string `"42"` then share one entry.

The ordering parameter `o` accepts `pk` as an alias for the fields behind the primary key. [Filtering and Ordering Semantics](/v3/core-concepts/filtering-and-ordering-semantics.md#ordering-metadata) describes that alias.

## Choice Identifier Value Semantics

Every choice value that VUEDA sends is a string. The rule covers the inline `choices` lists in `model_fields` and `model_filtering`. It also covers the responses of the [List field choices](/v3/reference/api/rest/vueda.info/vueda.info_model_info_choices_list.md) and [List filterset field choices](/v3/reference/api/rest/vueda.info/vueda.info_model_info_filter_choices_list.md) endpoints. For a related model, the value is the related row's pk. For a `SlugRelatedField`, it is the value of the slug field. Integer, UUID, and other column types all arrive as JSON strings.

Filter choices omit empty values, because clearing a filter means leaving its query parameter out. Field choices keep a blank entry when the field declares one, so a form can offer it.

The server sends strings because a filter value reaches it as a query string, which has no number type. A string choice value can match that value directly. For a filter with several active values, [FilterChip](/v3/reference/api/vue/FilterChip.md) finds each value's label with strict equality, so a numeric choice value would show as `unknown`. Converting on the server gives every column type one form before any client comparison.

[Choice-Backed Fields and Lookup Models](/v3/guides/choices-and-lookups.md) gives the steps for choice-backed fields. [`ModelInfoChoicesViewSet`](/v3/reference/api/py/vueda.info.viewsets/ModelInfoChoicesViewSet.md) and [`ModelInfoFilterSetChoicesViewSet`](/v3/reference/api/py/vueda.info.viewsets/ModelInfoFilterSetChoicesViewSet.md) describe the endpoints.
