Skip to content

Validate value based on key format #1159

@pgoerglerIaD

Description

@pgoerglerIaD

Hi,

How can I validate input based on the key name ?
If the key match /^foo(.+)$/ then it must be a string with maxLength 1
If the key match /^bar(.+)$/ then it must be a string with maxLength 10
otherwise no limitation
etc...

For example

{
  "foo(x)": "a",
  "bar(y)": "aaaaa",
  "z": "xxxxxxxxxxxxxxxx"
}

is valid
but this one is not

{
  "foo(x)": "aa"
}

because foo(x) must have 1 char max

I tried:

const FooSchema = v.record(v.pipe(v.string(), v.regex(/^foo\(.+\)$/)), v.pipe(v.string(), v.maxLength(1)))
const BarSchema = v.record(v.pipe(v.string(), v.regex(/^bar\(.+\)$/)), v.pipe(v.string(), v.maxLength(10)))
const WildcardSchema = v.record(v.string(), v.number())

const Schema = v.union([
  FooSchema,
  BarSchema,
  WildcardSchema,
])

const result = v.safeParse(Schema, {
  'bar(a)': 'x',
  'foo(b)': 'xxxxxx',
  'z': 'xxxxxxxxxxxxxxxx'
});

It does not work it returns

Invalid format: Expected /^foo\(.+\)$/ but received "bar(a)"
and more

Activity

muningis

muningis commented on Apr 17, 2025

@muningis
Contributor

There's no first-class support for that, yet atleast. As it's needed for one of valibot codegen lib, it's likely it will come (but not guaranteed). However, there are some nuances with type-safety which makes it extra complicated.

As additional note, v.union is to validate against one of members, so it first tries to validate against FooSchema, then against BarSchema finally against WildcardSchema. Unfortunately, what works for merging multiple schemas (Merge Objects and Intersections) require use of object() of which none works with record, as it's different checks. Merge requires entries property, which record does not have, and intersection validates against all schema -> so, bar(aa) property will fail on FooSchema

Additionally, as mentioned this is bit complicated to define even with typescript, as you can not have wildcard properties when using pattern'ed keys

type Schema = {
  [key: `foo(${string})`]: string;
  [key: `bar(${string})`]: string;
  [key: string]: number; // `string` widens two above keys into `string`, and now we have impossible type.
}

const data: Schema = {
  "foo(a)": "foo",
  "bar(bbb)": "bar",
  baz: 5
}

However, it works fine if there is no wildcard property. That type definition + v.custom makes it possible to implement that as per Playgroung example.

You could add additional check for wildcard, but I'm not sure how to get that typed into that type.

fabian-hiller

fabian-hiller commented on Apr 17, 2025

@fabian-hiller
Owner

I think that I would validate it with custom logic with check in the pipe of your object or record schema.

muningis

muningis commented on Apr 17, 2025

@muningis
Contributor

@fabian-hiller wdyt about adding first-class citizen schema for that? Only problem is inferring type literal, but I guess we could find inspiration in arktype :?

pgoerglerIaD

pgoerglerIaD commented on Apr 17, 2025

@pgoerglerIaD
Author

@muningis thanks i will check if i find a way for the wildcard

@fabian-hiller it is possible to know the key name when using check() ?

fabian-hiller

fabian-hiller commented on Apr 18, 2025

@fabian-hiller
Owner

Here is one quick example. You can try it out in our playground.

import * as v from 'valibot';

const Schema = v.pipe(
  v.record(v.string(), v.union([v.string(), v.number()])),
  v.rawCheck(({ dataset, addIssue }) => {
    if (dataset.typed) {
      for (const key in dataset.value) {
        const value = dataset.value[key];
        if (/^foo\(.+\)$/.test(key)) {
          if (typeof value !== 'string' || value.length > 1) {
            addIssue({
              message: 'Custom message 1',
              path: [
                {
                  type: 'object',
                  origin: 'value',
                  input: dataset.value,
                  key,
                  value,
                },
              ],
            });
          }
        } else if (/^bar\(.+\)$/.test(key)) {
          if (typeof value !== 'string' || value.length > 10) {
            addIssue({
              message: 'Custom message 2',
              path: [
                {
                  type: 'object',
                  origin: 'value',
                  input: dataset.value,
                  key,
                  value,
                },
              ],
            });
          }
        } else if (typeof value !== 'number') {
          addIssue({
            message: 'Custom message 3',
            path: [
              {
                type: 'object',
                origin: 'value',
                input: dataset.value,
                key,
                value,
              },
            ],
          });
        }
      }
    }
  }),
);

wdyt about adding first-class citizen schema for that?

I am open to discuss it, but it is important to me to think about a great API design first.

EskiMojo14

EskiMojo14 commented on Apr 20, 2025

@EskiMojo14
Contributor

Maybe something like the below?

v.objectWithPatterns(
  [
    [FooKeySchema, FooValueSchema],
    [BarKeySchema, BarValueSchema],
  ],
  WildcardSchema
);

It then wouldn't be too hard to infer the right types for each pattern: Playground

As @muningis said though, I don't know if the wildcard is possible to accurately represent with Typescript.

edit: after looking at what objectWithRest does (intersecting with another index signature), it does actually seem to work correctly:
Image
Image

For the keys you're essentially required to do the below every time:

const FooKeySchema = v.custom<`foo(${string})`>(arg => typeof arg === "string" && /^foo\(.+\)$/.test(arg))

It might be more convenient if there was an easier way of doing this, maybe:

const FooKeySchema = v.templateRegex<`foo(${string})`>(/^foo\(.+\)$/)

In a similar vein, though much more complex, Zod v4 adds a template literal type builder (which constructs a regexp instance internally) such as below:

z.templateLiteral(["foo(", z.string(), ")"])

I suspect it would be a major job to add a similar API to Valibot though.

linked a pull request that will close this issue on Apr 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestworkaroundWorkaround fixes problem

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    Participants

    @EskiMojo14@muningis@fabian-hiller@pgoerglerIaD

    Issue actions

      Validate value based on key format · Issue #1159 · fabian-hiller/valibot