353 lines
9.5 KiB
Markdown
353 lines
9.5 KiB
Markdown
---
|
|
title: "Fields Overview — Config, Validation, Virtual, Admin Options"
|
|
aliases: [payload-fields-overview, payload-field-config]
|
|
tags: [payloadcms, fields, validation, virtual-fields, admin-panel]
|
|
sources: [raw/fields__overview.md]
|
|
created: 2026-05-15
|
|
updated: 2026-05-15
|
|
---
|
|
|
|
# Fields Overview — Config, Validation, Virtual, Admin Options
|
|
|
|
Fields are the building blocks of Payload. They define the DB schema **and** auto-generate Admin Panel UI. This article covers shared config capabilities that apply across all field types: virtual fields, validation, default values, admin options, and custom components.
|
|
|
|
See [[wiki/payloadcms/fields-basic|Fields: Basic]] for scalar field types and [[wiki/payloadcms/fields-complex|Fields: Complex]] for structural/relational types.
|
|
|
|
---
|
|
|
|
## Field Categories
|
|
|
|
| Category | Purpose | `name` required? |
|
|
|----------|---------|-----------------|
|
|
| **Data** | Stored in DB | Yes |
|
|
| **Presentational** | Layout only (Row, Collapsible, unnamed Tabs/Group, UI) | No |
|
|
| **Virtual** | Computed/derived, not stored | Yes |
|
|
|
|
### Data Fields (full list)
|
|
|
|
Array, Blocks, Checkbox, Code, Date, Email, Group (named), JSON, Number, Point, Radio, Relationship, Rich Text, Select, Tabs (named), Text, Textarea, Upload
|
|
|
|
### Presentational Fields
|
|
|
|
Collapsible, Row, Tabs (unnamed), Group (unnamed), UI
|
|
|
|
### Virtual Fields
|
|
|
|
- **Join** — purpose-built virtual type for bi-directional relationships
|
|
- **Any field** with `virtual: true` or `virtual: 'path.to.value'`
|
|
|
|
---
|
|
|
|
## Virtual Field Configuration
|
|
|
|
Any data field type can be made virtual by adding the `virtual` property.
|
|
|
|
### Boolean virtual — computed via hook
|
|
|
|
```ts
|
|
{
|
|
name: 'fullName',
|
|
type: 'text',
|
|
virtual: true,
|
|
hooks: {
|
|
afterRead: [({ siblingData }) =>
|
|
`${siblingData.firstName} ${siblingData.lastName}`
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### String path virtual — resolves relationship data
|
|
|
|
```ts
|
|
{
|
|
name: 'authorName',
|
|
type: 'text',
|
|
virtual: 'author.name' // author relationship must exist on the same collection
|
|
}
|
|
```
|
|
|
|
**Path syntax rules:**
|
|
- Dot notation traverses relationships: `author.profile.bio`
|
|
- `hasMany` relationships return arrays: `categories.title` → `['Tech', 'News']`
|
|
- Source relationship field **must** exist in the same schema
|
|
- Resolved at query time, not stored
|
|
|
|
**Use cases:** display relationship names without ID, computed word counts, formatted summaries.
|
|
|
|
```json
|
|
// API response includes virtual fields alongside real ones
|
|
{
|
|
"id": "123",
|
|
"title": "My Post",
|
|
"author": "64f123...",
|
|
"authorName": "John Doe",
|
|
"categoryTitles": ["Tech", "News"],
|
|
"wordCount": 450
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Field Names
|
|
|
|
- Must be **unique** among siblings
|
|
- Follow JavaScript identifier conventions: start with letter or `_`, only letters/numbers/`_`
|
|
- **Avoid** hyphens (breaks GraphQL enum generation) and leading digits
|
|
- **Reserved names** (cannot be used): `__v`, `salt`, `hash`, `file`, `status` (with Postgres + drafts)
|
|
|
|
---
|
|
|
|
## Default Values
|
|
|
|
```ts
|
|
// Static
|
|
{ name: 'status', type: 'text', defaultValue: 'draft' }
|
|
|
|
// Dynamic function — runs on create/update
|
|
{
|
|
name: 'attribution',
|
|
type: 'text',
|
|
defaultValue: ({ user, locale, req }) =>
|
|
`${translation[locale]} ${user.name}`
|
|
}
|
|
```
|
|
|
|
Dynamic `defaultValue` receives: `user`, `locale`, `req` (use `req.payload` for Local API calls). Can be `async`.
|
|
|
|
---
|
|
|
|
## Validation
|
|
|
|
Fields auto-validate based on type and options (`required`, `min`, `max`). Override with `validate`:
|
|
|
|
```ts
|
|
{
|
|
name: 'myField',
|
|
type: 'text',
|
|
validate: (value, { user, data, siblingData, operation, id, req, event }) =>
|
|
Boolean(value) || 'This field is required'
|
|
}
|
|
```
|
|
|
|
Returns `true` (valid) or an error string.
|
|
|
|
### Validation Context (`ctx`)
|
|
|
|
| Property | Description |
|
|
|----------|-------------|
|
|
| `data` | Full document being edited |
|
|
| `siblingData` | Fields in same parent |
|
|
| `operation` | `'create'` or `'update'` |
|
|
| `path` | Array path: `['group', 'myArray', '1', 'field']` |
|
|
| `id` | Document ID (undefined during create) |
|
|
| `req` | HTTP request with `payload`, `user`, etc. |
|
|
| `event` | `'onChange'` or `'submit'` |
|
|
|
|
### Localized error messages
|
|
|
|
```ts
|
|
validate: (value, { req: { t } }) =>
|
|
Boolean(value) || t('validation:required')
|
|
```
|
|
|
|
### Reuse built-in validators
|
|
|
|
```ts
|
|
import { text } from 'payload/shared'
|
|
|
|
validate: (val, args) => {
|
|
if (val === 'bad') return 'Cannot be "bad"'
|
|
return text(val, args) // delegate to built-in
|
|
}
|
|
```
|
|
|
|
Available: `array, blocks, checkbox, code, date, email, json, number, point, radio, relationship, richText, select, tabs, text, textarea, upload`
|
|
|
|
### Validation performance
|
|
|
|
Admin Panel validates on **every change**. For expensive ops (DB queries), gate on `event`:
|
|
|
|
```ts
|
|
validate: async (val, { event }) => {
|
|
if (event === 'onChange') return true
|
|
const response = await fetch(`/api/check?val=${val}`)
|
|
return response.ok || 'Invalid value'
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Field-level Hooks
|
|
|
|
```ts
|
|
{
|
|
name: 'myField',
|
|
type: 'text',
|
|
hooks: {
|
|
beforeValidate: [...],
|
|
beforeChange: [...],
|
|
afterChange: [...],
|
|
afterRead: [...]
|
|
}
|
|
}
|
|
```
|
|
|
|
See [[wiki/payloadcms/hooks|Hooks]] for full details.
|
|
|
|
---
|
|
|
|
## Field-level Access Control
|
|
|
|
```ts
|
|
{
|
|
name: 'sensitiveData',
|
|
type: 'text',
|
|
access: {
|
|
read: ({ req: { user } }) => Boolean(user?.isAdmin),
|
|
create: () => false,
|
|
update: ({ req: { user } }) => Boolean(user?.isAdmin),
|
|
}
|
|
}
|
|
```
|
|
|
|
See [[wiki/payloadcms/access-control|Access Control]] for full details.
|
|
|
|
---
|
|
|
|
## Custom ID Fields
|
|
|
|
Override auto-generated ID:
|
|
|
|
```ts
|
|
fields: [
|
|
{
|
|
name: 'id',
|
|
type: 'number', // or 'text'
|
|
required: true,
|
|
}
|
|
]
|
|
```
|
|
|
|
- Only `number` or `text` types allowed
|
|
- Text IDs must not contain `/` or `.` characters
|
|
|
|
---
|
|
|
|
## Admin Options
|
|
|
|
Configured via `admin: {}` on any field:
|
|
|
|
| Option | Description |
|
|
|--------|-------------|
|
|
| `condition` | `(data, siblingData, ctx) => boolean` — show/hide field |
|
|
| `components` | Swap individual UI parts (see Custom Components below) |
|
|
| `description` | Help text (string, function, or React component) |
|
|
| `position` | `'sidebar'` or `'main'` (default) |
|
|
| `width` | CSS width — useful in `row` fields |
|
|
| `style` | Inline CSS on root element |
|
|
| `className` | CSS class on root element |
|
|
| `readOnly` | UI-only; no effect on API |
|
|
| `disabled` | Hide from all admin surfaces (`true`) or specific ones (object) |
|
|
| `hidden` | Converts to `<input type="hidden">` — still submits |
|
|
|
|
### `admin.disabled` granular control
|
|
|
|
```ts
|
|
admin: { disabled: { column: true, filter: true } }
|
|
// Keys: field, column, filter, groupBy, bulkEdit
|
|
```
|
|
|
|
UI fields default to `disabled: { bulkEdit: true }`.
|
|
|
|
### Conditional Logic
|
|
|
|
```ts
|
|
{
|
|
name: 'greeting',
|
|
type: 'text',
|
|
admin: {
|
|
condition: (data, siblingData, { blockData, path, user }) => {
|
|
return Boolean(data.enableGreeting)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Custom Components
|
|
|
|
Swap any part of a field's UI via `admin.components`:
|
|
|
|
| Component | Renders in | Notes |
|
|
|-----------|-----------|-------|
|
|
| `Field` | Edit View — the input | Use `useField()` hook to manage value |
|
|
| `Cell` | List View — table cell | Use `DefaultCellComponentProps` / `DefaultServerCellComponentProps` |
|
|
| `Filter` | List View — filter dropdown | Receives `disabled, onChange, operator, value` |
|
|
| `Label` | Anywhere labels appear | |
|
|
| `Error` | Below input on validation fail | |
|
|
| `Description` | Below label | Use component for dynamic/reactive help text |
|
|
| `Diff` | Version Diff View | Only visible when versioning is enabled |
|
|
| `beforeInput` | Before `<input>` element | Array of components |
|
|
| `afterInput` | After `<input>` element | Array of components |
|
|
|
|
### Field component — managing value
|
|
|
|
```tsx
|
|
'use client'
|
|
import { useField } from '@payloadcms/ui'
|
|
|
|
export const CustomTextField = () => {
|
|
const { value, setValue } = useField()
|
|
return <input onChange={(e) => setValue(e.target.value)} value={value} />
|
|
}
|
|
```
|
|
|
|
### TypeScript for custom components
|
|
|
|
```ts
|
|
import type {
|
|
TextFieldClientComponent,
|
|
TextFieldServerComponent,
|
|
TextFieldLabelClientComponent,
|
|
TextFieldDescriptionClientComponent,
|
|
TextFieldErrorClientComponent,
|
|
TextFieldDiffClientComponent,
|
|
} from 'payload'
|
|
```
|
|
|
|
Convention: `{FieldType}{ComponentRole}{ServerOrClient}Component` (e.g. `TextFieldLabelClientComponent`)
|
|
|
|
### Cell component example
|
|
|
|
```tsx
|
|
'use client'
|
|
import type { DefaultCellComponentProps } from 'payload'
|
|
|
|
export const PriceCellComponent: React.FC<DefaultCellComponentProps> = ({ cellData, rowData }) => {
|
|
const currency = rowData.currency || 'USD'
|
|
return <span>{new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(cellData)}</span>
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Key Takeaways
|
|
|
|
- **Three categories**: Data (stored), Presentational (layout), Virtual (computed/derived)
|
|
- **Any field can be virtual** with `virtual: true` (hook-computed) or `virtual: 'path.string'` (relationship path)
|
|
- **Virtual path fields** auto-resolve relationship data without a DB column; `hasMany` → returns arrays
|
|
- **Reserved field names** (`__v`, `salt`, `hash`, `file`, `status` with Postgres+drafts) cause silent config sanitization
|
|
- **Validation** runs on every keystroke in Admin — use `event === 'onChange'` guard for expensive async checks
|
|
- **`validate` returns** `true` (valid) or an error message string
|
|
- **`defaultValue`** can be async; receives `{ user, locale, req }` — use `req.payload` for Local API
|
|
- **`admin.disabled`** accepts boolean or object with keys `{ field, column, filter, groupBy, bulkEdit }`
|
|
- **Custom components** registered via path strings in `admin.components`; use `useField()` to wire value
|
|
- **TypeScript types** follow `{FieldType}{Role}{Env}Component` naming convention
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- `raw/fields__overview.md` — https://payloadcms.com/docs/fields/overview
|