The Power of Virtual Fields in Payload CMS
Virtual fields in Payload CMS are dynamically computed, read-only fields that derive their value at runtime. This article covers use cases like full name generation and estimated reading time, with no redundant data stored.
When working with Payload CMS, there are cases where API values should not be manually entered by a user or persistently stored in the database. Instead, these values are computed on the fly or derived from other data — this is where virtual fields truly shine.
This article covers what virtual fields are, how and when to use them, and provides practical real-world examples.
What Are Virtual Fields?
Virtual fields in Payload CMS are dynamically computed fields that are not persisted in the database. Instead, they are populated during the document read process using an afterRead hook. This hook runs after the document is read from the database but before it's returned to the caller, making it the perfect place to inject derived or computed values.
Because virtual fields don’t exist in the database, they are read-only, not queryable, cannot be used for sorting, and are not eligible for use as useAsTitle.
How to make a field virtual in Payload CMS?
Since Payload v3.0.0, you can define a virtual field by simply adding the virtual: true property to the field configuration. This tells Payload not to store the field in the database. Its value can then be populated dynamically, typically using an afterRead hook.
How to Set the Value of Virtual Fields
There are two common ways to populate virtual fields in Payload CMS, depending on your use case:
Via a Field Hook
This is the most common and straightforward approach, ideal when setting the value of a single virtual field. The logic stays encapsulated in the afterRead hook within the field definition.
import { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
fields: [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{
name: 'fullName',
type: 'text',
virtual: true,
admin: { readOnly: true },
hooks: {
afterRead: [
({ siblingData }) =>
[siblingData.firstName, siblingData.lastName].filter(Boolean).join(' '),
],
},
},
],
} Via a Collection Hook
This method is useful when multiple virtual fields need to be set and depend on shared logic or related data. For example, if a slug and parent field are stored on the document and a virtual path and alternatePaths field should be derived, it makes sense to use a beforeRead or afterRead Collection Hook and compute them together.
Use Cases
Virtual fields are especially useful when augmenting documents with values that can be dynamically calculated or derived, without storing them in the database. Below are some of the most common and practical scenarios:
Combining Data from Non-Virtual Fields
The most simple and common use case is combining existing fields within the same document.
Example: Combine First and Last Name
Here’s a simple example that generates a full name based on two existing fields: firstName and lastName.
import { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
fields: [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{
name: 'fullName',
type: 'text',
virtual: true,
admin: { readOnly: true },
hooks: {
afterRead: [
({ siblingData }) =>
[siblingData.firstName, siblingData.lastName].filter(Boolean).join(' '),
],
},
},
],
} This is useful in many real-world scenarios, such as user profiles, author displays, or contact lists, where it’s beneficial to have the full name computed.
Calculations Based on Existing Fields
Another common pattern is performing simple calculations from field values from a sibling field data.
Example: Estimated Reading Time for Blog Posts
Consider a blog that should show readers how long each post takes to read. Instead of entering this value manually, it can be calculated automatically from the post content using a virtual field.
import { CollectionConfig } from 'payload'
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical'
export const BlogPosts: CollectionConfig = {
slug: 'blog-posts',
fields: [
{ name: 'content', type: 'richText' },
{
name: 'readingTime',
type: 'number',
virtual: true,
admin: { readOnly: true },
hooks: {
afterRead: [
({ siblingData }) => {
const plaintext = convertLexicalToPlaintext({ data: siblingData.content })
const wordCount = plaintext.split(/\s+/).length
return Math.ceil(wordCount / 200)
},
],
},
},
],
} 💡 In this example, convertLexicalToPlaintext is a utility function offered by Payload that converts rich text or lexical editor content into plain text for analysis.Merging or Injecting Internal or External Data (Async Virtual Fields)
More advanced use cases include asynchronously enriching documents with data from other collections or external APIs.
import { CollectionConfig } from 'payload'
export const Locations: CollectionConfig = {
slug: 'locations',
fields: [
{ name: 'city', type: 'text' },
{
name: 'weather',
type: 'text',
virtual: true,
admin: { readOnly: true },
hooks: {
afterRead: [
async ({ siblingData }) => {
const res = await fetch(`https://wttr.in/${siblingData.city}?format=3`)
return res.text()
},
],
},
},
],
} Ideal for use cases like pulling the latest exchange rates, showing real-time weather conditions based on a location field, or displaying related data from another collection.
Good to Know When Using Virtual Fields
-
virtual: truealone doesn’t make a field read-only, useadmin.readOnly: trueto prevent users from entering unsaved values in the admin panel. - Virtual fields that rely solely on an
afterReadhook to populate data based on another field won’t display a value when creating a new document and won’t update live in the admin panel when the source field changes. Use a custom component to enable real-time behavior. - If marked
required, virtual fields can fail when theafterReadhook hasn’t run yet, disable validation withvalidate: () => true.
Why Use Virtual Fields?
Virtual fields offer an elegant way to extend documents with dynamically generated fields based on other data. They are ideal when separating static data from dynamically derived values, keeping the database lean and focused on raw, user-managed content.
Key benefits include:
- Always in Sync: Virtual fields are generated on the fly, meaning they always reflect the current state of the source field.
- Great for Presentation-Only Values: Virtual fields are ideal for values used purely for display, like labels or summaries.
- Clear Separation of Concerns: They maintain a clear separation between static and computed content.
- Perfect for read-only derived data: Useful for exposing values like full names, reading time, or computed slugs that don’t require user input.
When Not to Use Virtual Fields
While powerful, virtual fields are not always the best choice. Avoid them when:
- Filtering or Sorting Is Required: Virtual fields are not stored in the database and therefore cannot be filtered or sorted in queries.
- The Data Should Persist Over Time: If a value should be preserved historically (e.g., reading time at the time of publication), virtual fields are not suitable.
- Complex or Async Logic Is Involved: Heavy computations or async fetches (e.g., external API calls) can slow down read performance, especially with large datasets.
Alternatives to Virtual Fields
- Stored Derived Fields with Sync Logic: Calculated values (like reading time) can be stored in the database and updated via hooks (
beforeChange,afterChange) when source fields change. This allows querying and sorting, but introduces syncing complexity. - Admin-Editable Fields: If a field needs to be adjusted manually or occasionally overridden, a regular stored field with an optional default is more appropriate.
Relationship Virtual Fields
Payload V3.35.0 introduced a powerful new capability to virtual fields: the ability to link a virtual field value directly to a specific property of a related document.
What are Relationship Virtual Fields
A relationship virtual field lets you pull in a specific field from a related document (defined via a relationship or upload field) and expose it as part of the parent document—without persisting that field’s value in the database.
The special thing is that this field will always be populated with the corresponding value, even if the current depth is 0, Moreover, in contrast to non relationship virtual fields, they can also be queried and sorted by.
So to speak, virtual fields, pick or extract a field from a related document. They can even pick deeply nested fields like person.location.city
In the following example, each entry in the authors collection references a document from the persons collection via a person relationship field. A virtual name field is then used to dynamically mirror the title of the linked person:
import { CollectionConfig } from 'payload'
export const Authors: CollectionConfig = {
slug: 'authors',
fields: [
{
name: 'person',
type: 'relationship',
relationTo: 'persons',
},
{
name: 'name',
type: 'text',
virtual: 'person.title',
},
],
} When to use Relationship Virtual Fields
Use relationship virtual fields when:
- A specific field from a related document (e.g.
category.title) should be exposed without duplicating data. - Documents should be filtered or sorted based on a specific field from a related document.
- A field from a related document should serve as the title of the current document via
useAsTitle.
⚠️ Note: The relationship field used for a virtual field must be a single, non-polymorphic relationship. Fields with hasMany: true or polymorphic relations are not supported.Conclusion
Virtual fields are a powerful feature in Payload CMS that allow documents to be dynamically enriched without bloating the schema or storing redundant data. Whether it’s full names, reading times, or complex API responses, virtual fields keep the database lean and the API expressive.