Skip to content

Commit e09c619

Browse files
authored
fix: skip default validation for virtual fields (#16015)
### What? Virtual fields (`virtual: true` or `virtual: "path"`) no longer get a default type-based validator during field sanitization. They receive a noop validator instead. ### Why? Virtual fields are populated server-side (via hooks, built-in population, or plugins), so their value is `undefined` in form state. The default validator causes false validation errors in the admin UI that users cannot resolve. ### How? In `sanitize.ts`, when assigning a default validator, check if the field is virtual first. If so, assign `() => true` instead of the type-based validator. Explicit `validate` functions are still preserved. Fixes #16012 Related #16013 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1213743778134481
1 parent 7e2a126 commit e09c619

2 files changed

Lines changed: 103 additions & 5 deletions

File tree

packages/payload/src/fields/config/sanitize.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,4 +605,98 @@ describe('sanitizeFields', () => {
605605
expect(sanitizedBlock.admin?.disableBlockName).toStrictEqual(undefined)
606606
})
607607
})
608+
609+
describe('virtual fields', () => {
610+
it('should assign a noop validate for virtual: true fields', async () => {
611+
const fields: Field[] = [
612+
{
613+
name: 'virtualText',
614+
type: 'text',
615+
virtual: true,
616+
},
617+
]
618+
619+
const sanitizedField = (
620+
await sanitizeFields({
621+
config,
622+
collectionConfig,
623+
fields,
624+
validRelationships: [],
625+
})
626+
)[0] as TextField
627+
628+
expect(sanitizedField.validate).toBeDefined()
629+
expect(sanitizedField.validate!('', {} as any)).toBe(true)
630+
expect(sanitizedField.validate!(undefined as any, {} as any)).toBe(true)
631+
})
632+
633+
it('should assign a noop validate for virtual: "string" fields', async () => {
634+
const fields: Field[] = [
635+
{
636+
name: 'virtualRef',
637+
type: 'text',
638+
virtual: 'post.title',
639+
},
640+
]
641+
642+
const sanitizedField = (
643+
await sanitizeFields({
644+
config,
645+
collectionConfig,
646+
fields,
647+
validRelationships: [],
648+
})
649+
)[0] as TextField
650+
651+
expect(sanitizedField.validate).toBeDefined()
652+
expect(sanitizedField.validate!(undefined as any, {} as any)).toBe(true)
653+
})
654+
655+
it('should not override an explicit validate on a virtual field', async () => {
656+
const customValidate = () => true as const
657+
const fields: Field[] = [
658+
{
659+
name: 'virtualText',
660+
type: 'text',
661+
virtual: true,
662+
validate: customValidate,
663+
},
664+
]
665+
666+
const sanitizedField = (
667+
await sanitizeFields({
668+
config,
669+
collectionConfig,
670+
fields,
671+
validRelationships: [],
672+
})
673+
)[0] as TextField
674+
675+
expect(sanitizedField.validate).toBe(customValidate)
676+
})
677+
678+
it('should assign default type-based validate for non-virtual fields', async () => {
679+
const fields: Field[] = [
680+
{
681+
name: 'normalText',
682+
type: 'text',
683+
},
684+
]
685+
686+
const sanitizedField = (
687+
await sanitizeFields({
688+
config,
689+
collectionConfig,
690+
fields,
691+
validRelationships: [],
692+
})
693+
)[0] as TextField
694+
695+
expect(sanitizedField.validate).toBeDefined()
696+
// Non-virtual text field should use the text validator which checks required/minLength/etc.
697+
// Passing undefined with required should fail
698+
const result = sanitizedField.validate!(undefined as any, { required: true, req: { payload: { config: {} }, t: ((v: string) => v) as any } } as any)
699+
expect(result).not.toBe(true)
700+
})
701+
})
608702
})

packages/payload/src/fields/config/sanitize.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,16 @@ export const sanitizeFields = async ({
271271
}
272272

273273
if (typeof field.validate === 'undefined') {
274-
const defaultValidate = validations[field.type as keyof typeof validations]
275-
if (defaultValidate) {
276-
field.validate = (val: any, options: any) =>
277-
defaultValidate(val, { ...field, ...options })
278-
} else {
274+
if ('virtual' in field && field.virtual) {
279275
field.validate = (): true => true
276+
} else {
277+
const defaultValidate = validations[field.type as keyof typeof validations]
278+
if (defaultValidate) {
279+
field.validate = (val: any, options: any) =>
280+
defaultValidate(val, { ...field, ...options })
281+
} else {
282+
field.validate = (): true => true
283+
}
280284
}
281285
}
282286

0 commit comments

Comments
 (0)