Skip to content
Want deep learning about Payload? Payload Essentials is for you!Enroll Now

Payload CMS Fields: Blocks, Relationship, Upload, Join, and more

PayloadPayload CMS10:12 video • ~9 min readFree

In this video you'll learn the structure every field shares and more.

A field in Payload CMS defines what gets stored in your database and what the editor sees in the admin panel. In this post I'll walk through the options every field shares, then the four fields that need more than that: blocks, the relationship field, the upload field, and the join field. By the end you'll be able to read any field config in the docs, and you'll have two collections connected in both directions.

This post builds on my post about collections. If you don't have a collection set up yet, start there and come back to this one.

Everything here is Payload 3 (I'm on 3.88.0). Payload 4 is on the way as I write this, so expect an update shortly after it releases.

What is a field in Payload?

A field does two jobs. The first is defining what gets stored in your database. The second is defining what the editor sees in the admin panel.

That's why Payload has so many field types. They aren't data types. Each one is a data type paired with an interface:

  • A text field stores a string and gives you a text input.
  • A date field stores a date and gives you a date picker.
  • A relationship field stores an ID and gives you a searchable dropdown.

Once you know how one field works, you can read all of them. So let's do that first, and then spend the rest of the post on the few that need more time.

The options every field shares

I'll show you how all fields work on the title field we already have, because every other type takes the same options.

  • name and type are required on every data field. That's the minimum, and we covered it in the collections post.
  • required makes the field mandatory before a document can save. Payload's default validation will prevent you from saving the document if you haven't filled in the required field.
  • defaultValue fills the field in for you in new documents.
  • unique stops two documents having the same value.
  • index tells your database to index the column, which speeds up queries against it.
  • label changes what the editor sees, so your field can be named postTitle in code and read as Title in the panel.
  • admin gives you options to customize how your field renders. description puts help text under the input, position can move a field into the sidebar, readOnly shows the value without letting anyone change it, and condition shows or hides the field based on other values in the document.
  • access is set per field. This lets you set permissions field by field, so you can hide one field from a user who can otherwise read the whole document.
  • validate is a function you write to determine what can be put into a field, and it runs before save. Custom validation needs its own post, so we'll come back to it later.
  • hooks lets you run custom business logic at the field level. Hooks also get their own post.

Here are a few of those options together on the title field:

1{
2 name: 'title',
3 type: 'text',
4 required: true,
5 unique: true,
6 index: true,
7 admin: { description: 'Shown as the page heading.' },
8}

The three kinds of field

Payload groups fields into three kinds.

  1. Data fields save to your database. This includes text, number, select, date, checkbox, relationship, and about a dozen more.
  2. Presentational fields organise the admin panel but save nothing. Fields like rows, unnamed tabs, and collapsibles exist to make a long form readable.
  3. Virtual fields show up in your API responses and are never stored.

I'm not going to list every field type, because the options we just covered apply to all of them. Instead, I'll show you four fields that need more than that: blocks, relationship, upload, and join.

Blocks

The blocks field lets an editor build a page out of pieces that you define. You write the blocks. They choose which ones to use, and in what order.

Each block is its own object, with a slug and its own fields. First, I'll create a sample file called Blocks.ts just as an example (this isn't necessarily best practice). We'll import the Block type from Payload and create two blocks: a Hero with a required heading and a background image upload, and a Content block that accepts one rich text field.

1// Blocks.ts
2import type { Block } from 'payload'
3
4const Hero: Block = {
5 slug: 'hero',
6 fields: [
7 { name: 'heading', type: 'text', required: true },
8 { name: 'background', type: 'upload', relationTo: 'media' },
9 ],
10}
11
12const Content: Block = {
13 slug: 'content',
14 fields: [{ name: 'text', type: 'richText' }],
15}

You can create as many blocks as you want. Now, rather than passing those block objects straight into the field, define them once in your Payload config and reference them by slug. We do this by adding the blocks option to the config and importing the blocks from the file we just created.

payload.config.ts
1export default buildConfig({
2 blocks: [Hero, Content],
3})

Then, in your collection, add a blocks field. I'll call mine layout and add the blockReferences option with the slugs of my blocks.

1{
2 name: 'layout',
3 type: 'blocks',
4 blockReferences: ['hero', 'content'],
5 // for now we still need to include an empty blocks array
6 blocks: [],
7}

This is the recommended way, because a block defined once and referenced by slug doesn't get sent to the client over and over.

The empty blocks array is required. Payload's docs say it's there for compatibility, and Payload 4 removes the split, so the slugs go straight into blocks and blockReferences goes away.

Now an editor can stack a hero, then some content, then another hero, and drag them into whatever order they want.

The blocks are made of the same fields we already covered: a text field, an upload field, and a rich text field. Nothing new, just arranged. Blocks get more complicated than this, so they'll get their own post.

The Payload relationship field

The relationship field relates one document to another.

I've got the posts collection from the collections post, and I'll add an authors collection. Then, over in the Posts collection, I'll add a new field to the fields array named author, with the relationship type. relationTo is required, and it takes a collection slug.

Posts.ts
1{
2 name: 'author',
3 type: 'relationship',
4 relationTo: 'authors',
5}

That gives you a dropdown in the admin panel that searches the authors collection.

hasMany and polymorphic relationships

Now let's talk about a few field options you can use, starting with hasMany. hasMany lets you pick more than one author in this case. Set it to true and the field allows multiple inputs.

You can also pass more than one collection in as an array to relationTo. That's called a polymorphic relationship. When you do that, the stored value changes. Instead of just an ID, each entry becomes an object with the collection slug in relationTo and the ID in value.

Check what the field looks like in the API before you write your frontend code.

maxDepth

maxDepth caps how far Payload will populate this relationship, no matter what the request asks for. It's a ceiling you set on the field itself.

filterOptions

The last field option I'll discuss is filterOptions, which limits what shows up in the dropdown. Let's add an active checkbox to authors, because we only want active authors to be selectable. Over in the Authors collection, I'll add a checkbox field called active and default it to true, so any new author is selectable without anyone ticking a box.

Users.ts
1{
2 name: 'active',
3 type: 'checkbox',
4 defaultValue: true,
5}

Back on the author relationship field, filterOptions accepts a function that receives a set of arguments. We pull relationTo and data out of those arguments, check the relationship, and return every author that isn't explicitly marked inactive.

1filterOptions: ({ relationTo, data }) =>
2 relationTo === 'authors' && { active: { not_equals: false } },

I used not_equals: false rather than equals: true. Any author created before we added that checkbox has no value stored at all, so equals: true would hide every one of them. not_equals: false includes anything nobody has explicitly switched off.

Admin options

There are a few admin options too:

  • allowCreate lets an editor make a new author without leaving the page.
  • allowEdit lets them edit one in place.
  • sortOptions sets the dropdown order.
  • isSortable lets them drag the selected ones around.

The Payload upload field

The upload field is a relationship field specialized for media.

It has the same required options as relationship fields: name, type, and relationTo set to your media collection. The blank template includes a media collection, so we'll use that in our relationTo. Back in Posts, I'll add one more field named coverImage with the upload type.

Posts.ts
1{
2 name: 'coverImage',
3 type: 'upload',
4 relationTo: 'media',
5}

You get a media picker instead of a text dropdown.

Upload fields with hasMany and filterOptions

relationTo takes an array here too, and it combines with hasMany the same way it does on the relationship field.

And filterOptions works here as well. You can store all media in one media collection and filter per field, so a cover image field only returns images and a downloads field only offers PDFs.

The join field

The relationship and upload fields point one direction. A post knows its author. The author knows nothing about its posts. The join field handles the other direction.

Join is a virtual field, which means it stores nothing. It surfaces the related documents through Payload's APIs and in the admin panel, without duplicating the data.

Two options are required. collection is the slug you're joining, and on is the field it joins on. Go over to the Authors collection and create a join field named posts, with the join type. It looks in the posts collection and joins on the field named author.

Users.ts
1{
2 name: 'posts',
3 type: 'join',
4 collection: 'posts',
5 on: 'author',
6}

This says "bring me the posts whose author field points at this document." Each author document now lists its posts underneath.

For this to work, the relationship has to exist already. You can't join from nothing. The posts collection needs the author relationship field before authors can join back to it.

There's also an optional where, which sets a default filter on what comes back.

Join isn't the only virtual field. You can add virtual: true to any field type to keep it out of the database.

Wrapping up

You now know the options every field shares, you've got two collections related to each other, and you can read that relationship from both sides.

If you'd like the full walkthrough of every Payload field and a complete project setup in order, that's what my course, Payload Essentials, covers.

Keep going