You’re writing a collection in Payload and you need to know what each field type stores, what your editors will see, and which one fits. The field list is the whole data model, so the choice decides your database columns, your API responses and your admin screens at once.
As of 12 November 2026, the latest stable release, Payload 3.93.0, ships 22 field types. Payload’s fields documentation gives each one its own page. This guide puts them side by side, then covers the options every field shares, layout fields, virtual fields, ordering, how to choose, and what Payload 4 changes.
We build client projects on Payload from our own project template, and every page in it is built from a blocks field.
- How fields work in Payload
- Every Payload field type at a glance
- Fields that hold one value
- Fields that link documents: relationship, upload and join
- The rich text field
- Arrays and blocks for repeating content
- Group, tabs, row and collapsible: arranging the admin
- UI fields, custom components and colour pickers
- Virtual fields
- Options every field shares
- Letting editors sort and reorder
- How to choose a field type
- What changes for fields in Payload 4
- How we use fields on our builds
- Frequently asked questions
- What to do next
How fields work in Payload
A field is one entry in a collection’s or a global’s fields array, and that one entry does four jobs. It defines a database column (or a nested value on MongoDB), an input in the admin panel, a key in every API response, and a property in your generated TypeScript types. Every field has a type, and every field that stores data also has a name.
Payload sorts its types into three kinds. Data fields store a value. Presentational fields, such as a row or an unnamed tab, only arrange the admin. Virtual fields show a value that isn’t stored. Fields behave the same way in a collection and in a global.
For example, take a group of three boutique hotels in Dubai that wants one Payload backend for its rooms, its hotels and its website pages. The rooms collection starts with three fields:
import type { CollectionConfig } from 'payload'
export const Rooms: CollectionConfig = {
slug: 'rooms',
admin: { useAsTitle: 'name' },
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'maxGuests', type: 'number', min: 1, max: 6 },
{
name: 'bedType',
type: 'select',
options: ['king', 'queen', 'twin'],
},
],
}Editors now see a text box, a number input and a dropdown, and the API returns name, maxGuests and bedType on every room. If you haven’t set up a project yet, our Payload CMS tutorial builds the first collection from a blank template.
Every Payload field type at a glance
The table lists all 22 types in payload@3.93.0, grouped by kind. "Stores" is what lands in the database; "Admin shows" is what an editor works with.
| Type | Kind | Stores | Admin shows | Use it for |
|---|---|---|---|---|
text | data | a string, or a list of strings with hasMany | a text input | names, titles, slugs |
textarea | data | a string | a multi-line box | short plain descriptions |
email | data | a string, checked as an email | an email input | contact addresses |
number | data | a number, or a list with hasMany | a number input | prices, counts, ratings |
checkbox | data | true or false | a checkbox | flags such as "featured" |
select | data | one option, or several with hasMany | a dropdown | a fixed list of choices |
radio | data | one option | a radio group | two to five visible choices |
date | data | a date and time | a date picker | publish dates, opening times |
point | data | a longitude and latitude pair | two number inputs | locations for map queries |
code | data | a string | a code editor | snippets, embed code |
json | data | any JSON value | a JSON editor | settings, imported data |
relationship | data | a link to one or more documents | a searchable picker | authors, categories, related items |
upload | data | a link to a document in an upload collection | a file picker with a preview | images and files |
richText | data | formatted content as JSON | a rich text editor | article bodies |
array | data | a list of rows with the same fields | repeatable rows | amenities, opening hours |
blocks | data | a list of rows, each one of several shapes | a block picker | page layouts |
group | data when named | a nested object | a titled set of fields | SEO settings, addresses |
tabs | data when a tab is named | a nested object per named tab | tabs | splitting long forms |
row | layout | nothing | fields side by side | short related inputs |
collapsible | layout | nothing | a panel editors can fold away | optional or advanced settings |
ui | layout | nothing | your own component | previews, buttons, help |
join | virtual | nothing | a list of documents that point here | a hotel’s rooms |
Fields that hold one value
Text and numbers
text, textarea and email store strings. text takes minLength and maxLength, and hasMany: true turns it into a list of strings, which suits tags typed by hand. number takes min and max, and it also accepts hasMany.
Slugs aren’t a field type. Payload exports a slugField() helper that adds a text field and a hidden checkbox, and by default it fills the slug from title on save and puts both in the sidebar.
Choices
select and radio both take an options list. select renders a dropdown and accepts hasMany: true for several choices, while radio shows every option at once and stores one. A checkbox stores true or false, and in 3.93.0 a required checkbox with no defaultValue gets false as its default.
For example, the hotel’s rooms use a select for the bed type and a seaView checkbox.
Dates and time zones
A date field stores a full date and time. admin.date.pickerAppearance sets what the editor picks, from dayOnly (the default) to dayAndTime, timeOnly or monthOnly.
Set timezone: true and Payload adds a second, hidden field named after the first with _tz on the end, which stores the time zone the editor chose. For example, each hotel’s check-in time:
{
name: 'checkInTime',
type: 'date',
timezone: { defaultTimezone: 'Asia/Dubai' },
admin: { date: { pickerAppearance: 'timeOnly' } },
}Location, code and JSON
point stores coordinates and gets a 2dsphere index by default, so you can query by distance. It isn’t supported on SQLite, as our Payload database guide notes. code stores a string in a code editor with syntax highlighting for the language you set, and json stores any JSON value in a JSON editor.
Fields that link documents: relationship, upload and join
A relationship field stores a reference to another document. Point relationTo at one collection, or at an array of collections for a polymorphic link, and add hasMany: true for several. filterOptions limits what the picker offers. For example, a room links to its hotel:
{
name: 'hotel',
type: 'relationship',
relationTo: 'hotels',
required: true,
}An upload field works the same way, but only for collections set up for uploads, such as media. It stores a reference to the media document and shows a preview of the file.
A join field stores nothing. It lists the documents in another collection whose relationship points at this one. For example, the hotels collection can show every room that belongs to it:
{
name: 'rooms',
type: 'join',
collection: 'rooms',
on: 'hotel',
}The link lives only on the room, so there’s one source of truth, and the hotel’s edit view still shows its rooms.
The rich text field
A richText field stores the editor’s document as JSON. The editor comes from the config’s editor setting or the field’s own, and in 3.93.0 a rich text field with neither stops the config loading. Lexical (@payloadcms/richtext-lexical) is Payload’s own editor and the only one supported from 4.0, and you choose what it can do through the features you pass. For example, a table inside an article comes from the editor’s table feature, which 3.93.0 exports as EXPERIMENTAL_TableFeature.
Rich text suits content a writer formats. If the parts have meaning of their own, such as a price, a date or a link to a room, give them their own fields so the API can filter and sort by them.
Arrays and blocks for repeating content
An array holds rows that share one shape. Each row has the fields you list, and minRows and maxRows set the limits. For example, each room’s amenities:
{
name: 'amenities',
type: 'array',
maxRows: 12,
fields: [{ name: 'label', type: 'text', required: true }],
}A blocks field holds rows too, but each row picks its shape from a list of blocks. That makes it the usual way to build pages, since an editor stacks a hero, some text and a call to action in any order:
{
name: 'layout',
type: 'blocks',
minRows: 1,
blocks: [
{
slug: 'hero',
fields: [
{ name: 'heading', type: 'text', required: true },
{ name: 'image', type: 'upload', relationTo: 'media' },
],
},
{
slug: 'roomList',
fields: [{ name: 'rooms', type: 'relationship', relationTo: 'rooms', hasMany: true }],
},
],
}Each stored row records its blockType. On MongoDB, arrays and blocks sit inside the document. On Postgres and SQLite, each array gets a table of its own and so does each block type, unless you set the adapter’s blocksAsJSON option, which stores blocks as one JSON column. Our guide to Payload’s databases shows what that looks like for a real collection.
You can define blocks once in the config’s top-level blocks and reference them by slug with blockReferences, which keeps one shared definition across collections. If the rows are things other pages reuse, such as rooms, keep them in their own collection and link them with a relationship.
Group, tabs, row and collapsible: arranging the admin
row puts fields side by side, and each field’s admin.width sets its share. collapsible puts fields in a panel that opens and closes. Neither stores anything.
group and tabs work both ways. Without a name, they only arrange the admin, and the fields inside are stored at the top level. With a name, they store a nested object. For example, a pages collection with Content and SEO tabs:
{
type: 'tabs',
tabs: [
{
label: 'Content',
fields: [{ name: 'title', type: 'text', required: true }],
},
{
name: 'seo',
label: 'SEO',
fields: [{ name: 'metaTitle', type: 'text' }],
},
],
}The API returns title at the top level and seo.metaTitle inside an object, because only the second tab has a name.
In other words, adding a name to an existing tab or group moves its data to a new key. On Postgres and SQLite that changes the columns, so plan it as a migration before it reaches production.
UI fields, custom components and colour pickers
A ui field stores nothing and renders a component you write in the edit view or the list view. It still needs a name, and its component goes in admin.components.Field. For example, the hotel group could show a live booking-widget preview beside each room.
Any data field can also swap its input. Set admin.components.Field to your own component and the field keeps its type, validation and storage while editors see your interface. Our Payload admin panel guide covers custom components in more depth.
How to add a colour picker
Payload 3.93.0 has no colour field type. You get one in one of two ways:
- A
textfield that stores the hex value, with a customFieldcomponent that renders a picker. Payload’s docs on swapping in your own React components cover the pattern. - A community plugin that does the same thing for you. Several exist; check that one supports your Payload version before you install it.
For example, each hotel’s accent colour for its microsite is a text field with a picker on top. Colour inside rich text comes from the editor’s TextStateFeature instead, set on the rich text field.
Virtual fields
A virtual field appears in API responses and the admin but isn’t stored. Any field type becomes virtual with the virtual option, which takes two forms.
virtual: true leaves the value empty, and you fill it in a field hook, usually afterRead. For example, a room’s display label built from its name and bed type. virtual set to a path reads a value through a relationship:
{ name: 'hotelName', type: 'text', virtual: 'hotel.name' }Now each room returns its hotel’s name, and editors can see it in the list view without opening the hotel.
In 3.93.0, virtual fields are read-only in the admin unless you set admin.readOnly: false. A path virtual field can be used in queries and sorts, because Payload rewrites it to the path. A virtual: true field can’t be queried, since there’s nothing stored to filter on.
Options every field shares
Every type takes these options on top of its own:
| Option | What it does |
|---|---|
name | The key in the database and the API |
label | The text editors see; generated from the name if you leave it out |
required | Rejects a save without a value |
unique | Rejects a value another document already has |
index | Adds a database index for faster queries |
defaultValue | A value, or a function that returns one, for new documents |
validate | Your own check, which returns true or an error message |
localized | Stores one value per language |
hidden | Keeps the value in the database but out of every API and the admin |
access | create, read and update rules for this field alone |
hooks | beforeValidate, beforeChange, afterChange, afterRead and beforeDuplicate |
admin | condition, description, position, readOnly, width and component swaps |
For example, the rooms collection makes name required and gives maxGuests a defaultValue of 2.
localized is covered in our Payload localisation guide, field-level access in our authentication guide, and hooks as a whole in the Payload guide’s hooks section. Turning on drafts adds a _status field of its own, which our drafts and versions guide explains.
Field names to avoid
Names become keys and columns, so keep them to letters, numbers and underscores. In 3.93.0 the config throws an InvalidFieldName error for a name with a dot in it, and a ReservedFieldName error at start-up for three top-level names. These are file in an upload collection, and salt or hash in an auth collection with local login. For example, a field named file on the hotel group’s media collection stops Payload starting. Payload’s docs also list status (on Postgres with drafts on) and __v as reserved, so avoid both.
Letting editors sort and reorder
Editors drag rows in an array or a blocks field to reorder them, which is on by default. Set admin.isSortable: false to stop it. A hasMany relationship field becomes draggable when you set admin.isSortable: true.
Payload added drag-and-drop ordering for whole documents in 3.32.0 (April 2025). Set orderable: true on a collection and editors drag documents into order in the list view, while the API can set the same order. Payload adds a hidden, indexed _order field that holds each position as a fractional index, and sorts by it unless you set another default. That’s a new column on Postgres and SQLite, so it needs a migration. The option is marked experimental in the 3.93.0 types. A join field takes orderable: true as well, for ordering the documents it lists.
For example, the hotel group orders its rooms by hand so the suites come first on each hotel’s page. Sorting a query is a separate setting. Pass a field name to the sort parameter, with a minus sign in front for descending order.
How to choose a field type
Work through the questions for each piece of data. For example, the rooms collection:
- One value or many? Maximum guests is one number. Tags typed by hand are a
textfield withhasMany. - A fixed list or free input? The bed type has three options, so it’s a
select. A name is free text. - Data of its own, or another document? The hotel is a document with its own fields, so it’s a
relationship. A photo is a document in the media collection, so it’s anupload. - Rows of one shape, or of several? Amenities all have a label, so they’re an
array. A page mixes heroes and room lists, so it’sblocks. - Stored or worked out? The hotel’s name already lives on the hotel, so the room reads it through a virtual field.
- Data or layout? An SEO group that the front end reads is a named group or tab. Fields placed side by side for editors are a
row.
The finished collection:
import type { CollectionConfig } from 'payload'
export const Rooms: CollectionConfig = {
slug: 'rooms',
admin: { useAsTitle: 'name' },
fields: [
{
type: 'row',
fields: [
{ name: 'name', type: 'text', required: true, admin: { width: '60%' } },
{ name: 'maxGuests', type: 'number', min: 1, max: 6, admin: { width: '40%' } },
],
},
{ name: 'bedType', type: 'select', options: ['king', 'queen', 'twin'] },
{ name: 'seaView', type: 'checkbox' },
{ name: 'hotel', type: 'relationship', relationTo: 'hotels', required: true },
{ name: 'hotelName', type: 'text', virtual: 'hotel.name' },
{ name: 'photos', type: 'upload', relationTo: 'media', hasMany: true },
{
name: 'amenities',
type: 'array',
fields: [{ name: 'label', type: 'text', required: true }],
},
],
}What changes for fields in Payload 4
Payload 4 is in beta, and the 3.0 to 4.0 migration guide lists these changes for fields as of 4.0.0-beta.8 (10 November 2026). Betas can still change before 4.0 is stable, and our Payload review tracks the release.
blockReferencesis gone. Theblocksproperty takes both inline blocks and the slugs of config-level blocks, and a codemod moves you over.minandmaxonhasManyrelationship and upload fields are removed. UseminRowsandmaxRows.typescriptSchemais renamedjsonSchema.- Every block gets its own TypeScript interface, named from its slug, and
interfaceNameonly overrides the name. - Queries without
depthpopulate one level of relationships, down from two. - Collections and globals gain
createdByandupdatedByfields by default, which on Postgres and SQLite needs a migration. - The rich text table feature drops its prefix and becomes
TableFeature.
The admin side of 4.0 is in our admin panel guide’s Payload 4 section.
How we use fields on our builds
Our project template (v2.3.4, November 2026, on Payload 3.90.1) is what our client projects start from. In its shared collections, globals and page blocks, text carries most of the content by far, then checkbox, select, group and array, then upload, textarea and number, then a few relationships.
A page is a title, a slug, a publish date and one required blocks field. The template ships five content blocks (hero, rich text content, media, call to action and a listing block), defined inline on the page layout, and each project can register its own. Editors drag blocks and array rows into order, which is Payload’s default. The Content and SEO tabs on pages come from Payload’s SEO plugin.
We don’t use slugField(). Pages and tenants each have a plain text slug field with a field hook that builds it from the title or name when it’s left empty. Our own field definitions use no join, point, code or radio fields, and no collection uses orderable. One of our custom field components wraps json fields so the code editor always loads from our own copy, never a CDN.
Some fields have to be added by people who don’t edit code. Our dynamic data plugin, one of the plugins we’ve written, lets a tenant admin pick from 13 field types, from text and date to relationship, attachment and formula. The plugin stores those definitions as data, so a new field needs no deploy and no migration.
If you’d like a data model shaped around your content before the build starts, our Payload CMS development team can help.
