Skip to content

fix(zod): treat @uuid without a version as any UUID version - #2832

Merged
ymc9 merged 1 commit into
zenstackhq:devfrom
MaxFreedomPollard:fix/uuid-attr-default-version
Sep 6, 2026
Merged

fix(zod): treat @uuid without a version as any UUID version#2832
ymc9 merged 1 commit into
zenstackhq:devfrom
MaxFreedomPollard:fix/uuid-attr-default-version

Conversation

@MaxFreedomPollard

@MaxFreedomPollard MaxFreedomPollard commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

A bare @uuid only accepts v4. The version argument is declared optional and stdlib.zmodel documents the attribute as "Validates a string field value is a valid UUID", so @uuid with no argument should accept any version, which is what the sibling isUuid() function already does.

model Foo {
    id  Int    @id @default(autoincrement())
    ref String @uuid
}
// v4, accepted
await db.foo.create({ data: { ref: '20ef31c8-a2c6-4dca-b87b-838e364ab4b3' } });

// v7, rejected
await db.foo.create({ data: { ref: '019ff964-2f1d-7668-9a76-8648f2af9146' } });
// Invalid create args for model "Foo": Validation error: Invalid UUID at "data.ref"

v7 is the shape ZenStack's own uuid(7) generator produces, so a value read out of one model and written into a @uuid field of another is rejected. Every non-v4 version fails, because the compiled pattern requires a literal 4 in the version nibble.

Cause

addStringValidation in packages/zod/src/utils.ts maps the attribute with if (version === 7) result.uuidv7() else result.uuidv4(). The else covers @uuid(4) and the no-version form alike, so a bare @uuid compiles to zod's v4-only pattern ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$, while packages/orm/src/client/crud/operations/base.ts:1119 generates uuid.v7() for uuid(7). The isUuid branch of evalCall in the same utils file passes version: undefined to z.uuid() when the argument is omitted, which accepts any version, so the attribute path and the function path disagreed on identical semantics.

Changes

@uuid(4) maps to uuidv4(), @uuid(7) to uuidv7(), and a bare @uuid to uuid(), which accepts any version. Those are the only three reachable branches, since the @uuid check in attribute-application-validator.ts already rejects every other version literal.

tests/e2e/orm/validation/toplevel.test.ts gains a str13 String? @uuid(4) field and four assertions: a bare @uuid accepts a v7 value, @uuid(7) rejects a v4 value, @uuid(4) rejects a v7 value, and @uuid(4) accepts a v4 value. The last three pin the versioned forms so this does not get fixed in the other direction later.

Verification

With the test change applied and packages/zod/src/utils.ts reverted to dev, TEST_DB_PROVIDER=sqlite vitest run orm/validation/toplevel.test.ts in tests/e2e fails "works with string fields" with Invalid UUID at "data.str11", and the reported pattern is the v4-only regex above. With the fix, that test passes.

pnpm --filter @zenstackhq/zod test is 537 passed, no type errors. pnpm --filter @zenstackhq/zod lint, tsc --noEmit and prettier --check on both changed files are clean.

Two tests in tests/e2e/orm/validation were not run locally: "works with list fields" and "works with custom validation" declare Int[] fields and open a Postgres connection, which fails with ECONNREFUSED 127.0.0.1:5432 on my machine. Both fail identically on unmodified dev.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected UUID validation so @uuid(4) accepts version 4 UUIDs and rejects version 7 UUIDs.
    • Preserved support for version 7 validation through @uuid(7).
    • Unversioned @uuid validation continues to accept UUID values across supported versions.
  • Tests

    • Expanded validation coverage for version-specific and unversioned UUID annotations.

`addStringValidation` in packages/zod/src/utils.ts mapped the `@uuid`
attribute with `if (version === 7) uuidv7() else uuidv4()`, so the
no-version form fell into the v4 branch and rejected every other UUID
version. stdlib.zmodel declares the version argument as optional and
documents the attribute as "Validates a string field value is a valid
UUID", and the `isUuid()` branch in the same file already passes an
undefined version to `z.uuid()`, which accepts any version.

Writing a v7 or v1 UUID into a `String @uuid` field therefore failed with
`Validation error: Invalid UUID`. v7 is the shape ZenStack's own `uuid(7)`
generator produces, at
packages/orm/src/client/crud/operations/base.ts:1119.

Only the no-version branch changes, to `result.uuid()`. `@uuid(4)` and
`@uuid(7)` stay pinned, and the `@uuid` check in
attribute-application-validator.ts already rejects any other version
literal.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 85c50291-4135-4ca9-872c-361b2465c6bd

📥 Commits

Reviewing files that changed from the base of the PR and between a4a25c4 and 96e1526.

📒 Files selected for processing (2)
  • packages/zod/src/utils.ts
  • tests/e2e/orm/validation/toplevel.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Changes

UUID validation

Layer / File(s) Summary
UUID version routing
packages/zod/src/utils.ts
@uuid(4) uses uuidv4(), @uuid(7) uses uuidv7(), and unpinned versions use uuid().
End-to-end UUID coverage
tests/e2e/orm/validation/toplevel.test.ts
The schema adds a @uuid(4) field. Tests cover unpinned UUIDs and explicit version 4 and 7 validation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 96e15

Bare @uuid now accepts UUIDs across versions, while @uuid(4) and @uuid(7) remain pinned to their respective versions. The changed behavior is covered by end-to-end assertions, with no active merge-readiness risk identified.

Suggested reviewers: sanny-io

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: bare @uuid validation now accepts any UUID version.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/zod/src/utils.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9 ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making this PR @MaxFreedomPollard . LGTM!

@ymc9
ymc9 merged commit 3f1ed72 into zenstackhq:dev Sep 6, 2026
8 checks passed
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.

2 participants