valibot
valibot copied to clipboard
feat: create recordWithPatterns schema
fixes #1159
const Schema = v.recordWithPatterns(
[
[
v.pipe(v.string(), v.regex(/^foo\(.+\)$/)),
v.pipe(v.string(), v.maxLength(1)),
],
[
v.pipe(v.string(), v.regex(/^bar\(.+\)$/)),
v.pipe(v.string(), v.maxLength(10)),
],
],
v.number(),
);
Also includes proper index signatures for strongly typed keys, e.g.
const FooKeySchema = v.custom<`foo(${string}`>(
(arg) => typeof arg === "string" && /^foo\(.+\)$/.test(arg),
);
const Schema = v.recordWithPatterns([[FooKeySchema, v.string()]], v.number());
const parsed = v.parse(Schema, { "foo(x)": "x", x: 1 });
parsed["foo(x)"]; // string
parsed.x; // number
The latest updates on your projects. Learn more about Vercel for Git ↗︎
| Name | Status | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| valibot | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Apr 22, 2025 10:11am |
Thank you for creating this PR! Do you know if Zod offers such a schema? How about calling it recordWithPatterns or templateRecord to better match our record schema? Is the second argument, the wildcard schema, necessary? Maybe it simplifies the API if we ask users to add this to the first argument.
Do you know if Zod offers such a schema?
not to my knowledge, no - JSON schema has patternProperties which is similar
How about calling it recordWithPatterns or templateRecord to better match our record schema?
sure, makes sense
Is the second argument, the wildcard schema, necessary? Maybe it simplifies the API if we ask users to add this to the first argument.
what would be the desired behaviour if a key doesn't match any of the patterns?
Ah, the rest argument is necessary, because these two types behave differently:
type Unmerged = {
[key: `foo(${string})`]: string;
[key: `bar(${string})`]: number;
} & {
[key: string]: boolean;
}
type Merged = {
[key: `foo(${string})`]: string;
[key: `bar(${string})`]: number;
[key: string]: boolean;
}
in Merged, the string index signature overrides all of the others.
edit: hmm, the below works, so we could just avoid calling Prettify to merge the intersection:
type Unmerged = {
[key: `foo(${string})`]: string;
} & {
[key: `bar(${string})`]: number;
} & {
[key: string]: boolean;
}
it's kinda ugly, but 🤷🏻
that does still leave the question of what to do when a key doesn't match any of the patterns:
const parsed = v.parse(
v.recordWithPatterns([
[v.pipe(v.string(), v.startsWith("foo-")), v.string()],
]),
{ "foo-allowed": "hi", notAllowed: true },
);
// loose - { 'foo-allowed': 'hi', notAllowed: true }
// strip - { 'foo-allowed': 'hi' }
// strict - error
// rest - validate against rest, error if not match