obsidian/wiki/payloadcms/fields-relationship.md
2026-05-15 15:34:33 +01:00

168 lines
5.7 KiB
Markdown

---
title: "Relationship Field"
aliases: [relationship-field, payload-relationship, payload-polymorphic-relationship]
tags: [payloadcms, fields, relationship, polymorphic, hasMany]
sources: [raw/fields__relationship.md]
created: 2026-05-15
updated: 2026-05-15
---
One of the most powerful Payload fields — links documents across collections, supports polymorphic (multi-collection) references, and works with the [[wiki/payloadcms/fields-join|Join Field]] for bi-directional authoring.
## Config Options
```ts
import type { Field } from 'payload'
export const MyRelationshipField: Field = {
name: 'owner',
type: 'relationship',
relationTo: 'users', // string or string[] for polymorphic
hasMany: false, // true = array of relations
}
```
| Option | Notes |
|--------|-------|
| `relationTo` * | Collection slug(s) — array = polymorphic |
| `hasMany` | `true` → array mode |
| `minRows` / `maxRows` | Validation for `hasMany` |
| `filterOptions` | `Where` query or function to limit selectable docs |
| `maxDepth` | Override global populate depth for this field |
| `unique` | DB-level unique index |
| `localized` | Per-locale separate relation values |
## Admin Options
| Property | Notes |
|----------|-------|
| `isSortable` | Drag-and-drop reorder (requires `hasMany: true`) |
| `allowCreate` | Disable inline doc creation (default: `true`) |
| `allowEdit` | Disable inline doc edit (default: `true`) |
| `appearance` | `'select'` (default) or `'drawer'` |
| `sortOptions` | String or `{ collectionSlug: 'fieldName' }` — prefix `-` for desc |
| `placeholder` | Custom placeholder text or function |
## Data Shapes
### Has One (single collection)
Config: `relationTo: 'users'`, `hasMany: false`
```json
{ "owner": "6031ac9e1289176380734024" }
```
Query: `?where[owner][equals]=6031ac9e1289176380734024`
### Has One — Polymorphic (multiple collections)
Config: `relationTo: ['users', 'organizations']`, `hasMany: false`
```json
{
"owner": {
"relationTo": "organizations",
"value": "6031ac9e1289176380734024"
}
}
```
Query by value: `?where[owner.value][equals]=6031ac9e1289176380734024`
Query by collection: `?where[owner.relationTo][equals]=organizations`
### Has Many (single collection)
Config: `relationTo: 'users'`, `hasMany: true`
```json
{ "owners": ["6031ac9e1289176380734024", "602c3c327b811235943ee12b"] }
```
Query: `?where[owners][equals]=6031ac9e1289176380734024`
### Has Many — Polymorphic
Config: `relationTo: ['users', 'organizations']`, `hasMany: true`
```json
{
"owners": [
{ "relationTo": "users", "value": "6031ac9e1289176380734024" },
{ "relationTo": "organizations", "value": "602c3c327b811235943ee12b" }
]
}
```
Query: `?where[owners.value][equals]=6031ac9e1289176380734024`
## Filtering Relationship Options
```ts
filterOptions: ({ relationTo, siblingData, data, id, req, user }) => {
if (relationTo === 'products') {
return { stock: { greater_than: siblingData.quantity } }
}
return true // no filter
}
```
- Return `Where` query, `true` (no filter), or `false` (block all)
- Used for both UI filtering AND server-side validation
- **Gotcha:** if you also provide a custom `validate` function, call the default Payload validator from `payload/shared` inside it — otherwise `filterOptions` is not enforced on the backend
## Bi-directional Relationships
The Relationship field alone is **one-way** (the related document has no back-reference).
Use [[wiki/payloadcms/fields-join|Join Field]] on the related collection to add a virtual reverse lookup without storing extra data.
## Polymorphic Query Limitation
> You **cannot** query on a field *within* a related document when the relationship is polymorphic.
Only `field.value` (doc ID) and `field.relationTo` (collection slug) are safe to query. Filtering by a sub-field (e.g. `field.name`) breaks because that field may not exist across all related collections.
## Auto-population via Depth
Use the [[wiki/payloadcms/queries|Depth]] query param to automatically populate related documents in API responses:
- `?depth=1` → expand one level of relationships
- `maxDepth` on the field overrides the remaining depth
## Custom Components
### Field (Server)
```tsx
import { RelationshipField } from '@payloadcms/ui'
import type { RelationshipFieldServerComponent } from 'payload'
export const CustomRelationshipFieldServer: RelationshipFieldServerComponent =
({ clientField, path, schemaPath, permissions }) => (
<RelationshipField field={clientField} path={path} schemaPath={schemaPath} permissions={permissions} />
)
```
### Field (Client)
```tsx
'use client'
import { RelationshipField } from '@payloadcms/ui'
import type { RelationshipFieldClientComponent } from 'payload'
export const CustomRelationshipFieldClient: RelationshipFieldClientComponent =
(props) => <RelationshipField {...props} />
```
## Key Takeaways
- `relationTo` as a **string array** = polymorphic — data shape changes to `{ relationTo, value }` wrapper
- Polymorphic data is **always** `{ relationTo, value }` — even for `hasMany: false`
- Polymorphic querying: only `field.value` (ID) and `field.relationTo` (slug) — never sub-fields
- `hasMany: true` + single collection → flat ID array; `hasMany: true` + polymorphic → array of `{ relationTo, value }` objects
- Use [[wiki/payloadcms/fields-join|Join Field]] for reverse/bi-directional lookup (zero DB overhead)
- `filterOptions` filters both UI dropdown **and** server-side validation — but only if default validator is called when a custom `validate` is present
- `admin.isSortable` requires `hasMany: true`
- `admin.appearance: 'drawer'` is better for large-document relationships
## Sources
- `raw/fields__relationship.md` — https://payloadcms.com/docs/fields/relationship