Skip to content

fix: point field validation accepts a half-filled coordinate pair when a coordinate is 0 - #17599

Open
shuvamk wants to merge 1 commit into
payloadcms:mainfrom
shuvamk:fix/point-validation-zero-coordinate
Open

fix: point field validation accepts a half-filled coordinate pair when a coordinate is 0#17599
shuvamk wants to merge 1 commit into
payloadcms:mainfrom
shuvamk:fix/point-validation-zero-coordinate

Conversation

@shuvamk

@shuvamk shuvamk commented Aug 2, 2026

Copy link
Copy Markdown

What?

On an optional point field, a coordinate pair where the entered coordinate is 0 and the other coordinate is missing or unparseable passes validation:

Value Expected Actual on main
[5, null] validation:invalidInput validation:invalidInput
[0, null] validation:invalidInput true
[null, 0] validation:invalidInput true
['', 0] validation:invalidInput true
[0, 'abc'] validation:invalidInput true
['abc', 0] validation:invalidInput true

Every longitude except 0 is rejected when the latitude is blank. 0 alone slips through.

Payload's own pipeline produces this shape. packages/payload/src/fields/hooks/beforeValidate/promise.ts:140 normalises each coordinate before the validator runs — a string that is empty after trimming becomes null, anything else goes through parseFloat. So a REST or GraphQL submit of { point: ['0', ''] } is canonicalised into exactly [0, null], which then validates. The admin UI reaches the same state: packages/ui/src/fields/Point/index.tsx seeds the value as [null, null] and handleChange writes parseFloat(e.target.value) for the edited coordinate only, so typing 0 into Longitude and leaving Latitude blank yields [0, null].

I have not traced what the adapters do with the half-filled array once it is past validation, so I am not claiming a specific downstream symptom — everything below is scoped to the validator, which is what the added tests cover.

Why?

packages/payload/src/fields/validations.ts:

if ((value[1] && Number.isNaN(lng)) || (value[0] && Number.isNaN(lat))) {
  return t('validation:invalidInput')
}

The cross-indexing is deliberate and correct — "a latitude was provided, so the longitude must parse to a number", and vice versa. The defect is that presence is tested with truthiness. 0 is a valid longitude (Greenwich) and a valid latitude (equator), but it is falsy, so a 0 coordinate is read as "not provided" and the pairing check never fires. The two bounds guards below use the same truthiness test.

How?

Replace the truthiness presence test with an explicit null / undefined / '' check, and reuse it for the bounds guards so the whole function agrees on what "provided" means:

const hasLng = value[0] !== null && value[0] !== undefined && value[0] !== ''
const hasLat = value[1] !== null && value[1] !== undefined && value[1] !== ''

The bounds guards are unchanged behaviourally — 0 is in range on both axes — but leaving them on value[0] && next to hasLng in the same function would be confusing.

Scope: this only affects optional point fields. required: true already returns validation:requiresTwoNumbers for every input in the table above, and that path is untouched.

Tests

Six cases added to the existing describe('point') block in packages/payload/src/fields/validations.spec.ts — four for the broken inputs, two pinning the other direction ([0, 0] and the ['', ''] default both still validate).

I verified the new tests fail without the source change. Stashing only validations.ts and keeping the spec:

× should prevent zero longitude with a missing latitude
× should prevent zero latitude with a missing longitude
× should prevent zero latitude with an empty longitude
× should prevent zero longitude with a text latitude

Tests  4 failed | 20 passed | 72 skipped (96)

Restoring the fix:

Tests  24 passed | 72 skipped (96)

All 18 pre-existing point tests pass unchanged.

Full unit suite (vitest run --project unit), same machine, same commit:

  • baseline on main: 2 failed | 1785 passed | 2 skipped (1789)
  • with this change: 2 failed | 1791 passed | 2 skipped (1795)

+6, no change to the failures. Both pre-existing failures are local-environment artifacts of my checkout (tools/releaser/src/lib/publishList.spec.ts shells out to pnpm turbo; packages/payload/src/bin/build.spec.ts asserts a resolved vite bin path), plus packages/typescript-plugin/src/__tests__/plugin.spec.ts failing to collect. They are unrelated to this change and reproduce on a clean tree.

ESLint on validations.ts: 0 errors; the 4 remaining warnings are pre-existing and on lines 382/954/1019. Prettier clean. tsc --noEmit on packages/payload reports no errors other than the pre-existing TS6305 unbuilt-project-reference noise.

Note on #17538

#17538 edits this same line, but reads the cross-indexing as a typo and un-crosses it:

-  if ((value[1] && Number.isNaN(lng)) || (value[0] && Number.isNaN(lat))) {
+  if ((value[0] && Number.isNaN(lng)) || (value[1] && Number.isNaN(lat))) {

That removes the pairing check entirely and makes [5, null] valid. It fails the existing should prevent missing value test ([0.1]), which I confirmed by applying it locally:

FAIL  packages/payload/src/fields/validations.spec.ts > Field Validations > point > should prevent missing value
AssertionError: expected true not to be true

Flagging it so the two don't get merged in a way that silently reverts this. Happy to rebase around whichever lands first.

Alternative

The presence test could be narrowed further — e.g. treating any non-numeric coordinate as absent so [0, 'abc'] returns requiresTwoNumbers instead of invalidInput. I kept invalidInput because that is what ['bad', 'input'] already returns on an optional field, and changing it would alter messages for inputs that are currently handled correctly. Happy to switch if you'd prefer the other message.

…n a coordinate is 0

The pairing check in the `point` validator tests coordinate presence with
truthiness. `0` is a valid longitude (Greenwich) and a valid latitude
(equator) but is falsy, so a `0` coordinate reads as "not provided" and the
cross-indexed pairing check never fires.

On an optional point field:

  [5, null]    -> validation:invalidInput   (correct)
  [0, null]    -> true                      (should be invalidInput)
  [null, 0]    -> true                      (should be invalidInput)
  ['', 0]      -> true                      (should be invalidInput)
  [0, 'abc']   -> true                      (should be invalidInput)

This is reachable from the admin UI: the Point field seeds its value as
[null, null] and its onChange writes parseFloat(input) per coordinate, so
typing `0` into Longitude and leaving Latitude blank produces [0, null].
Every other longitude is rejected; only `0` slips through, and the
half-filled array then reaches the database adapter.

Replace the truthiness presence test with an explicit null/undefined/''
check, and reuse it for the bounds guards below so the whole function agrees
on what "provided" means. The bounds guards are unaffected behaviourally
since `0` is in range on both axes.

Required point fields already return validation:requiresTwoNumbers for these
inputs, so this only changes optional fields.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant