fabian-hiller / valibot

The modular and type safe schema library for validating structural data 🤖
https://valibot.dev
MIT License
5.6k stars 169 forks source link

Custom (runtime) error message #647

Closed IlyaSemenov closed 3 weeks ago

IlyaSemenov commented 3 weeks ago

As a developer using Valibot, I often want to generate validation error messages in runtime based on user input or other data. I didn't find a documented way to do so. In all examples, the error messages are predefined/hardcoded.

Consider the simplified schema:

import * as v from "valibot"

const schema = v.pipe(v.string(), v.check((x) => {
  const [c1, c2, c3] = x
  if (c1 === "f" && c2 !== c3) {
    const message = `Since the first letter is "${c1}", the two next letters ("${c2}" and "${c3}") should equal.`
    // <--- How to pass this message to res.issues?
    return false
  }
  return true
}))

const res = v.safeParse(schema, "fox")
console.log(res)

Throwing Error or v.ValiError doesn't work, the error is not captured and safeParse throws itself. Besides, ValiError is not developer-friendly at all (one is supposed to pass 6 (!) non-optional non-obvious arguments).

What I would expect, is some helper available in v.check and v.transform callbacks, such as:

throw v.createError({ message, expected, ... }) // only message is required
throw v.createError(message) // shortcut for string message only
fabian-hiller commented 3 weeks ago

Instead of a string, you can also pass a function to any schema and validation action to dynamically generate the error message. The first argument of this function gives you access to the issue object. Also, I will soon start working on a refine action (issue #597) that will give you full control for such advanced cases.

const Schema = v.string((issue) => `Expected: ${issue.expected}`);
IlyaSemenov commented 3 weeks ago

Thanks you for the heads up. However, this way of dynamically generating messages is quite limited. What I mean is, arguably, more often than not these dynamic messages will be based on validation context. In my example above, the message depends on extracted characters. This way, I basically need to repeat the logic twice, first in check and then in the message generator.

In my real life use case, I am validating a textarea value which is supposed to store the whitespace delimited list of wallets where each wallet must be a valid base58-encoded public key. The validation is supposed to report the particular malformed string, or which keys are duplicate, or when it's not enough or too many of them and why — all and all, that are mostly context-dependent messages.

I hope refine will make this possible!

fabian-hiller commented 3 weeks ago

Please have a look if rawCheck solves this issue for you: https://github.com/fabian-hiller/valibot/issues/597#issuecomment-2168353406

IlyaSemenov commented 3 weeks ago

It does:

const schema = v.pipe(v.string(), v.rawCheck(({ dataset, addIssue }) => {
  if (dataset.typed) {
    const [c1, c2, c3] = dataset.value
    if (c1 === "f" && c2 !== c3) {
      addIssue({ message: `Since the first letter is "${c1}", the second and third letters ("${c2}" and "${c3}") should equal.` })
    }
  }
}))

I'm still not really following why do we need the obligatory if (dataset.typed) boilerplate in each and every check, but at least it gets things done, thank you.

fabian-hiller commented 3 weeks ago

It does

I think I was able to simplify your scheme. However, your version is probably a bit more readable.

import * as v from 'valibot';

const Schema = v.pipe(
  v.string(),
  v.check(
    ([c1, c2, c3]) => c1 !== 'f' || c2 == c3,
    ({ input: [c1, c2, c3] }) =>
      `Since the first letter is "${c1}", the second and third letters ("${c2}" and "${c3}") should equal.`,
  ),
);

I'm still not really following why do we need the obligatory if (dataset.typed) boilerplate in each and every check, but at least it gets things done, thank you.

This is because I am working on validation actions that allow you to validate untyped data if the part of the data you want to validate is typed. This is really important for form validation. Without this functionality, we can't show errors that depend on multiple fields if any other field is untyped.

IlyaSemenov commented 3 weeks ago

Thank you for the reply. Anyhow, this issue is fully resolved so I'm closing it.