obsidian/wiki/payloadcms/fields-overview.md
2026-05-15 15:32:01 +01:00

9.5 KiB

title aliases tags sources created updated
Fields Overview — Config, Validation, Virtual, Admin Options
payload-fields-overview
payload-field-config
payloadcms
fields
validation
virtual-fields
admin-panel
raw/fields__overview.md
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: 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

{
  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
  • 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.

// 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 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

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) 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