From 4a85800220aa0ab105c2eb6506f832304362d87e Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:54:44 -0700 Subject: [PATCH] fix(type): keep uppercase UUIDs valid in string.uuid JSON Schema The string.uuid regexes relied on the i flag for uppercase hex digits. toJsonSchema only exports the regex source as `pattern`, so the flag was dropped and the exported schema rejected uppercase UUIDs that the runtime check accepts. List both cases in the character classes instead, as string.hex already does, so runtime validation is unchanged and the exported pattern matches the same strings. Co-Authored-By: Claude Opus 5.5 --- ark/type/__tests__/keywords/uuid.test.ts | 17 +++++++++++++++++ ark/type/keywords/string.ts | 18 +++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/ark/type/__tests__/keywords/uuid.test.ts b/ark/type/__tests__/keywords/uuid.test.ts index 3bd966dd5..0c0ec9864 100644 --- a/ark/type/__tests__/keywords/uuid.test.ts +++ b/ark/type/__tests__/keywords/uuid.test.ts @@ -25,6 +25,23 @@ contextualize(() => { ) }) + it("JSON Schema pattern accepts uppercase like the runtime check", () => { + const upper = validUuidV4.toUpperCase() + + const Uuid = type("string.uuid") + attest(Uuid(upper)).equals(upper) + const uuidSchema = Uuid.toJsonSchema() as { + anyOf: { pattern?: string }[] + } + const versionedPattern = uuidSchema.anyOf[0].pattern! + attest(new RegExp(versionedPattern, "u").test(upper)).equals(true) + + const Uuidv4 = type("string.uuid.v4") + attest(Uuidv4(upper)).equals(upper) + const v4Pattern = (Uuidv4.toJsonSchema() as { pattern: string }).pattern + attest(new RegExp(v4Pattern, "u").test(upper)).equals(true) + }) + it("rejects partial matches", () => { const Uuid = type("string.uuid") attest(Uuid("dbb1e8e0-40fc-4c14-87eb-61b25d166a1b extra").toString()).snap( diff --git a/ark/type/keywords/string.ts b/ark/type/keywords/string.ts index 0779d70a5..2b76509c3 100644 --- a/ark/type/keywords/string.ts +++ b/ark/type/keywords/string.ts @@ -841,37 +841,37 @@ export const uuid = Scope.module( "#nil": "'00000000-0000-0000-0000-000000000000'", "#max": "'ffffffff-ffff-ffff-ffff-ffffffffffff'", "#versioned": - /^[\da-f]{8}-[\da-f]{4}-[1-8][\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[1-8][\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, v1: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-1[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-1[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv1" ), v2: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-2[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-2[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv2" ), v3: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-3[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-3[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv3" ), v4: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-4[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv4" ), v5: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-5[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-5[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv5" ), v6: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-6[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-6[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv6" ), v7: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-7[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-7[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv7" ), v8: regexStringNode( - /^[\da-f]{8}-[\da-f]{4}-8[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i, + /^[\dA-Fa-f]{8}-[\dA-Fa-f]{4}-8[\dA-Fa-f]{3}-[89ABab][\dA-Fa-f]{3}-[\dA-Fa-f]{12}$/, "a UUIDv8" ) },