---
title: "belongs-to"
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# belongs-to

<!-- Generated by `pnpm docs:fields` from packages/core/src/schema/fields/belongs_to.ts. Edit the TSDoc, not this file. -->

Picker for a to-one relation.

| | |
| --- | --- |
| Builder | `F.belongsTo(attribute: string): BelongsTo` |
| Wire type key | `belongs-to` |
| Class chain | `BelongsTo` → `RelationField` → `Field` |
| Detail projection | `relation-entry` |
| Column counterpart | — *(not tabulatable)* |

## Example

```ts
import { F, when } from '@adonia/core'

F.belongsTo('authorId')                // relation inferred: `author`
  .resource('users')                   // target inferred when omitted
  .optionLabel('fullName')
  .searchable()                        // urls.options, no inline options
  .creatable()                        // inline-create modal (D2-5)
  .required()

F.belongsTo('categoryId').preload(50).optionLabel('name').nullable()
```

## Props

Beyond the shared [field DSL](/reference/fields#the-shared-field-dsl):

| Prop | Declared on | Behaviour |
| --- | --- | --- |
| `resource(slug: string): this` | `RelationField` | Names the target resource explicitly (§7.3 `{ resource }`). Without it the target is inferred from the Lucid relation, which is right whenever exactly one resource manages the related model. |
| `relation(name: string): this` | `RelationField` | Lucid relation this field reads and writes. Defaults to the attribute with a `Id`/`_id` suffix stripped — `authorId` → `author` — which is the Lucid convention for a `belongsTo` foreign key. |
| `optionLabel(attribute: string): this` | `RelationField` | Target attribute rendered as the option label, overriding the target's `recordTitle` (§6.1). |
| `searchable(enabled = true): this` | `RelationField` | Renders the picker as an async combobox backed by the `urls.options` capability URL (protocol §4) instead of a static list. NO options ride the descriptor — that is the entire point: a resource with 200 000 rows must not serialize them into a form. |
| `preload(limit: number = RELATION_OPTIONS_PAGE_SIZE): this` | `RelationField` | Ships the first `limit` options inline as `props.options` while STILL emitting `urls.options` (protocol §4): the list renders without a round-trip, and typing past the preloaded page searches the rest. |
| `creatable(enabled = true): this` | `RelationField` | Lets the user create a target record from inside this form (§7.3), in a modal built from the TARGET resource's own descriptor. |
| `keyType(type: RelationKeyType): this` | `RelationField` | Primary-key shape of the target model, driving the §11.1 base (`vine.number()` vs `vine.string()`). Defaults to `'number'`: Lucid carries no SQL type metadata on `$columnsDefinitions`, so the shape cannot be introspected, and `increments('id')` is the overwhelming convention. UUID-keyed targets declare `.keyType('string')`. |

### Introspection

Read-only members this type adds — used by the compiler, the table derivation and
tests rather than by a schema author.

| Member | Declared on | Behaviour |
| --- | --- | --- |
| `get targetSlug(): string \| undefined` | `RelationField` | The declared target slug, when `.resource()` was called. |
| `get relationName(): string` | `RelationField` | The Lucid relation name (declared, or inferred from the attribute). |
| `get optionLabelAttribute(): string \| undefined` | `RelationField` | The declared option-label attribute, when any. |
| `get keyShape(): RelationKeyType` | `RelationField` | Primary-key shape of the target (§11.1). |
| `get preloadLimit(): number \| undefined` | `RelationField` | Inline-options page size, or `undefined` when `preload()` was not called. |
| `get isSearchable(): boolean` | `RelationField` | Whether the field offers server-side search (§7.3 `searchable()`). |
| `get capabilities(): readonly string[]` | `RelationField` | Protocol §4: `options`, unconditionally. |
| `detailProps(): JsonObject` | `RelationField` | Chrome plus the three props a `relation-entry` is defined to read (protocol §8). |
| `get columnType(): string \| undefined` | `RelationField` | Derives to a `relation` cell only when a label attribute is declared: a table column showing raw foreign keys is noise, and the dotted key `author.fullName` is also what registers the relation for preloading (§18). A per-instance decision, like a choice field's `badge`/`text` split. |
| `get columnKey(): string` | `RelationField` | `author.fullName` — the dotted path the derived `relation` cell reads (§7.5). |
| `contributeHints(hints: EagerHints): void` | `RelationField` | §18: register the relation so a detail/edit page hydrating through it reads materialized data. Without this a 40-field form with three relation fields costs three extra queries per render, and `belongs-to-many` hydration costs one per record. |
| `get optionsResolver(): OptionsResolver \| undefined` | `RelationField` | The built-in options resolver (protocol §4), unless the author declared their own through `dependentOptions()` — an explicit resolver is a deliberate override of the target's row set and must win. |
| `resolveTarget(ctx: HttpContextLike \| undefined): Promise<RelationTarget \| undefined>` | `RelationField` | Resolves the target once per field instance (a resource instance is per-request, so this is a per-request memo, not a global cache). |
| `preloadedOptions(ctx: HttpContextLike): Promise<FieldOption[]>` | `RelationField` | The first page of options, for `preload()` — the same resolution the endpoint uses, so inline and searched options are the same rows in the same order. |

## Validation

Vine base: `vine.string()`, `vine.number()`.

§11.1: `vine.number()` / `vine.string()` matching the target's primary
key, plus the `exists` rule against the target's scoped table.

Contributed by `BelongsTo.vineSchema()` in `packages/core/src/schema/fields/belongs_to.ts`. The shared modifiers (`required`, `nullable`, `requiredWhen`, `unique`, optionality) are applied on top by `Field.buildValidation`, and a `rules()` override replaces the base entirely — the validator compiler never branches on the field class.

## Declared in

- `packages/core/src/schema/fields/belongs_to.ts` — `BelongsTo`
- `packages/core/src/schema/fields/relation.ts` — `RelationField`

Source: https://adonia.pages.dev/reference/fields/belongs-to/index.mdx
