--- 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 `` — 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 `` element | Array of components | | `afterInput` | After `` element | Array of components | ### Field component — managing value ```tsx 'use client' import { useField } from '@payloadcms/ui' export const CustomTextField = () => { const { value, setValue } = useField() return 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 = ({ cellData, rowData }) => { const currency = rowData.currency || 'USD' return {new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(cellData)} } ``` --- ## 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