The Power of Virtual Fields in Payload CMS
Written by Jens Becker
Last updated on June 21, 2025

When working with Payload CMS , sometimes you want to expose values in your API that are not manually entered by a user, nor stored persistently in your database. Instead, these values are computed on the fly, derived from other data. This is where virtual fields truly shine.
In this article, we'll cover what virtual fields are, how and when to use them, and provide practical real-world examples. Let's dive in.
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 the 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 calculated data.
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 you're setting the value of a single virtual field. The logic stays encapsulated in the afterRead
hook within the field definition.
{
name: 'virtual-field',
type: 'text',
virtual: true,
hooks: {
afterRead: [
({ siblingData, req }) {
// Compute the virtual field data.
// Here you have access to the sibling field data, Payload request and more.
},
],
},
}
A simple virtual text field whose value is dynamically computed after the document data has been read from the database.
Via a Collection Hook
This method is useful when you need to set multiple virtual fields that depend on shared logic or related data. For example, if you have a slug
and parent
field stored on the document, and want to derive a virtual path
and alternatePaths
field, it makes sense to use a beforeRead
or afterRead
Collection Hook and compute them together.
⚠️ Note: In collection-level hooks like
afterRead
, you should first check if a given virtual field was requested (selected). This ensures that you're not computing unnecessary values when the field isn’t needed.
Use Cases
Virtual fields are especially useful when you want to augment your document with values that can be dynamically calculated or derived, without storing them in your 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
.
{
name: 'fullName',
type: 'text',
virtual: true,
hooks: {
afterRead: [
({ siblingData }) => {
return `${siblingData.firstName} ${siblingData.lastName}`;
},
],
},
}
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 automatically.
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
Suppose you're building a blog and want to show readers how long it will take to read each post. Instead of manually entering this value, you can calculate it from the post's content using a virtual field.
{
name: 'estimatedReadingTime',
type: 'number',
virtual: true,
hooks: {
afterRead: [
({ siblingData }) => {
const contentString = siblingData.content
? convertLexicalToPlaintext({ data: siblingData.content })
: '';
const wordCount = contentString.trim().split(/\s+/).length;
const wordsPerMinute = 238;
const readingTime = Math.ceil(wordCount / wordsPerMinute);
return readingTime;
},
],
},
}
💡 In this example, convertLexicalToPlaintext
is a utility function offered by Payload that converts your 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.
{
name: 'weatherData',
type: 'json',
virtual: true,
hooks: {
afterRead: [
async ({ siblingData }) => {
const res = await fetch(`https://weatherapi.com/forecast?city=${siblingData.city}`)
const weather = await res.json()
return weather
},
],
},
}
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, essentially acting as lightweight foreign data wrappers for enriching documents at read time.
Good to Know When Using Virtual Fields
virtual: true
alone doesn’t make a field read-only, useadmin.readOnly: true
to prevent users from entering unsaved values in the admin panel.- Virtual fields that rely solely on an
afterRead
hook 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 theafterRead
hook hasn't run yet, disable validation withvalidate: () => true
.
Why Use Virtual Fields?
Virtual fields offer an elegant way to extend your documents with dynamically generated fields based on other data. They’re ideal when you want to separate static data from dynamically derived values, keeping your 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 data. No manual resyncing is required.
- Great for Presentation-Only Values: Virtual fields are ideal for values used purely for display, like labels or summaries in your frontend. This keeps all the data in one place, your CMS.
- Clear Separation of Concerns: They maintain a clear separation between static and computed content
- Are perfect for read-only derived data: Useful for exposing values like full names, reading time, or computed slugs that don’t require user input or database storage.
When Not to Use Virtual Fields
While powerful, virtual fields are not always the best choice. Avoid them when:
- You Need to Query the Field: 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, virtual fields are not suitable, they will always recalculate.
- You’re Performing Complex or Async Logic: Heavy computations or async fetches (e.g. external API calls) can slow down read performance, especially in bulk reads like list views or APIs returning many documents.
Alternatives to Virtual Fields
- Stored Derived Fields with Sync Logic: You can store calculated values (like reading time) in the database and use hooks (
beforeChange
,afterChange
) to update them 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 optional auto-fill logic might be 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:
{
slug: 'authors',
admin: {
useAsTitle: 'name'
},
fields: [
{
type: 'relationship',
name: 'person',
relationTo: 'persons',
},
{
type: 'text',
name: 'name',
virtual: 'person.title',
},
],
},
Virtual name field on the authors collection which will always be set to the name of the person.
When to use Relationship Virtual Fields
Use relationship virtual fields when:
- You want to expose a specific field from a related document (e.g.
category.title
) without duplicating data. - You want to query or sort documents based on a specific field from a related document.
- You want to use a field from a related document 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 you to dynamically enrich your documents without bloating your schema or storing redundant data in the database. Whether you’re generating full names, reading times, or dynamic labels, they help keep your content model clean, efficient, and expressive.
Have a creative use case for virtual fields or questions about integrating them into your workflow? We’d love to hear from you! For feedback, suggestions, or anything else, feel free to reach out via email at info@jhb.software . Thanks for reading!