diff --git a/README.md b/README.md index 53368dd49..9b6a30e11 100644 --- a/README.md +++ b/README.md @@ -595,6 +595,7 @@ z.string().emoji(); z.string().uuid(); z.string().cuid(); z.string().cuid2(); +z.string().ulid(); z.string().regex(regex); z.string().startsWith(string); z.string().endsWith(string); diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index def27ddfc..576816765 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -96,6 +96,7 @@ export type StringValidation = | "regex" | "cuid" | "cuid2" + | "ulid" | "datetime" | "ip" | { startsWith: string } diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 4e1e5a5bb..787de5178 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -217,6 +217,16 @@ test("cuid2", () => { } }); +test("ulid", () => { + const cuid = z.string().cuid(); + cuid.parse("01ARZ3NDEKTSV4RRFFQ69G5FAV"); + const result = cuid.safeParse("invalidulid"); + expect(result.success).toEqual(false); + if (!result.success) { + expect(result.error.issues[0].message).toEqual("Invalid ulid"); + } +}); + test("regex", () => { z.string() .regex(/^moo+$/) diff --git a/deno/lib/types.ts b/deno/lib/types.ts index da20a76c0..2fc0d3234 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -497,6 +497,7 @@ export type ZodStringCheck = | { kind: "uuid"; message?: string } | { kind: "cuid"; message?: string } | { kind: "cuid2"; message?: string } + | { kind: "ulid"; message?: string } | { kind: "startsWith"; value: string; message?: string } | { kind: "endsWith"; value: string; message?: string } | { kind: "regex"; regex: RegExp; message?: string } @@ -519,6 +520,7 @@ export interface ZodStringDef extends ZodTypeDef { const cuidRegex = /^c[^\s-]{8,}$/i; const cuid2Regex = /^[a-z][a-z0-9]*$/; +const ulidRegex = /[0-9A-HJKMNP-TV-Z]{26}/; const uuidRegex = /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; // from https://stackoverflow.com/a/46181/1550155 @@ -710,6 +712,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "url") { try { new URL(input.data); @@ -827,10 +839,14 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); } + ulid(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + ip(options?: string | { version?: "v4" | "v6"; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } - + datetime( options?: | string @@ -952,6 +968,9 @@ export class ZodString extends ZodType { get isCUID2() { return !!this._def.checks.find((ch) => ch.kind === "cuid2"); } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } get isIP() { return !!this._def.checks.find((ch) => ch.kind === "ip"); } diff --git a/src/ZodError.ts b/src/ZodError.ts index 0802c1793..76344ee71 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -96,6 +96,7 @@ export type StringValidation = | "regex" | "cuid" | "cuid2" + | "ulid" | "datetime" | "ip" | { startsWith: string } diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index fa55796ca..7fc06958f 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -216,6 +216,16 @@ test("cuid2", () => { } }); +test("ulid", () => { + const cuid = z.string().cuid(); + cuid.parse("01ARZ3NDEKTSV4RRFFQ69G5FAV"); + const result = cuid.safeParse("invalidulid"); + expect(result.success).toEqual(false); + if (!result.success) { + expect(result.error.issues[0].message).toEqual("Invalid ulid"); + } +}); + test("regex", () => { z.string() .regex(/^moo+$/) diff --git a/src/types.ts b/src/types.ts index cd01a65e3..1df5cb81d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -497,6 +497,7 @@ export type ZodStringCheck = | { kind: "uuid"; message?: string } | { kind: "cuid"; message?: string } | { kind: "cuid2"; message?: string } + | { kind: "ulid"; message?: string } | { kind: "startsWith"; value: string; message?: string } | { kind: "endsWith"; value: string; message?: string } | { kind: "regex"; regex: RegExp; message?: string } @@ -519,6 +520,7 @@ export interface ZodStringDef extends ZodTypeDef { const cuidRegex = /^c[^\s-]{8,}$/i; const cuid2Regex = /^[a-z][a-z0-9]*$/; +const ulidRegex = /[0-9A-HJKMNP-TV-Z]{26}/; const uuidRegex = /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; // from https://stackoverflow.com/a/46181/1550155 @@ -710,6 +712,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "url") { try { new URL(input.data); @@ -826,6 +838,9 @@ export class ZodString extends ZodType { cuid2(message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); } + ulid(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } ip(options?: string | { version?: "v4" | "v6"; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); @@ -952,6 +967,9 @@ export class ZodString extends ZodType { get isCUID2() { return !!this._def.checks.find((ch) => ch.kind === "cuid2"); } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } get isIP() { return !!this._def.checks.find((ch) => ch.kind === "ip"); }