9.5 KiB
| title | aliases | tags | sources | created | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Fields Overview — Config, Validation, Virtual, Admin Options |
|
|
|
2026-05-15 | 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 for scalar field types and wiki/payloadcms/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: trueorvirtual: 'path.to.value'
Virtual Field Configuration
Any data field type can be made virtual by adding the virtual property.
Boolean virtual — computed via hook
{
name: 'fullName',
type: 'text',
virtual: true,
hooks: {
afterRead: [({ siblingData }) =>
`${siblingData.firstName} ${siblingData.lastName}`
]
}
}
String path virtual — resolves relationship data
{
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 hasManyrelationships 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.
// 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
// 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:
{
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
validate: (value, { req: { t } }) =>
Boolean(value) || t('validation:required')
Reuse built-in validators
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:
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
{
name: 'myField',
type: 'text',
hooks: {
beforeValidate: [...],
beforeChange: [...],
afterChange: [...],
afterRead: [...]
}
}
See wiki/payloadcms/hooks for full details.
Field-level Access Control
{
name: 'sensitiveData',
type: 'text',
access: {
read: ({ req: { user } }) => Boolean(user?.isAdmin),
create: () => false,
update: ({ req: { user } }) => Boolean(user?.isAdmin),
}
}
See wiki/payloadcms/access-control for full details.
Custom ID Fields
Override auto-generated ID:
fields: [
{
name: 'id',
type: 'number', // or 'text'
required: true,
}
]
- Only
numberortexttypes 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
admin: { disabled: { column: true, filter: true } }
// Keys: field, column, filter, groupBy, bulkEdit
UI fields default to disabled: { bulkEdit: true }.
Conditional Logic
{
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
'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
import type {
TextFieldClientComponent,
TextFieldServerComponent,
TextFieldLabelClientComponent,
TextFieldDescriptionClientComponent,
TextFieldErrorClientComponent,
TextFieldDiffClientComponent,
} from 'payload'
Convention: {FieldType}{ComponentRole}{ServerOrClient}Component (e.g. TextFieldLabelClientComponent)
Cell component example
'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) orvirtual: '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,statuswith Postgres+drafts) cause silent config sanitization - Validation runs on every keystroke in Admin — use
event === 'onChange'guard for expensive async checks validatereturnstrue(valid) or an error message stringdefaultValuecan be async; receives{ user, locale, req }— usereq.payloadfor Local APIadmin.disabledaccepts boolean or object with keys{ field, column, filter, groupBy, bulkEdit }- Custom components registered via path strings in
admin.components; useuseField()to wire value - TypeScript types follow
{FieldType}{Role}{Env}Componentnaming convention
Sources
raw/fields__overview.md— https://payloadcms.com/docs/fields/overview