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

How to know if a custom error message was given? #661

Closed tujoworker closed 1 week ago

tujoworker commented 2 weeks ago

Hi,

thank you for making this great library 🥇

When I use Valibot in a library that provides its custom strings. Like a error message for all kinds of required cases. I can check if the issue type is non_empty, non_optional etc. and return my message.

But what if the user of that library provides a message manually? How can I know that the given message was a custom one?

Do you have a workaround or could consider to add a property to the issue object?

fabian-hiller commented 2 weeks ago

Thank you very much! I need to see the code (or an example) to give a proper answer. Can you share it with me? In general, Valibot's error messages follow a hierarchy. The most specific messages available is applied and all others are ignored.

tujoworker commented 2 weeks ago

For now, I can customise the translations this way:

function getValidationMessage(issue: BaseIssue<unknown>, locale: Locale) {
  const tr = translations[locale]

  switch (issue.received) {
    case 'undefined':
      return tr.Field.errorRequired
  }

  switch (issue.type) {
    case 'non_empty':
    case 'non_optional':
      return tr.Field.errorRequired
  }

  return issue.message
}

But not all issues have the needed info to make this work nicely.

e.g. I want to mimic the Ajv exclusiveMinimum, which I do like so:

v.custom((value: number) => value > props.exclusiveMinimum)

But I would need to return more info to the issue I receive later (like type or at least the requirement). Is that possible?

tujoworker commented 2 weeks ago

Now I get Invalid type: Expected unknown but received 1 but can't change it to mye custom "translation", because I don't know that the exclusiveMinimum has initialised it.

fabian-hiller commented 2 weeks ago

If you upgrade to our latest version you can use rawCheck to fully control the issue information:

import * as v from 'valibot';

const Schema = v.pipe(
  v.number(),
  v.rawCheck(({ dataset, addIssue }) => {
    if (dataset.typed && dataset.value <= props.exclusiveMinimum) {
      addIssue({ message: 'Error!!!', expected: `>${props.exclusiveMinimum}`  });
    }
  })
);

For more info about custom error messages and i18n have a look at this guide and this comment.

tujoworker commented 2 weeks ago

Nice!

I want to both add type and requirement via addIssue but get this type error: type' does not exist in type 'IssueInfo$1<number>'. Any thoughts?

tujoworker commented 2 weeks ago

Different issue: I wonder, is it possible to get the path in the issue, likeissue.path, when setting a translation with setGlobalMessage?

fabian-hiller commented 2 weeks ago

Different issue: I wonder, is it possible to get the path in the issue, likeissue.path, when setting a translation with setGlobalMessage?

Yes, this should be possible.

I want to both add type and requirement via addIssue but get this type error: type' does not exist in type 'IssueInfo$1<number>'. Any thoughts?

Can you share your code with me? You can use our playground to do so. All issue info properties are optional and will be set automatically for you if you do not set them. The type should not be a custom string and the requirement should be null or the input requirement, e.g. props.exclusiveMinimum.

I am happy to work with you on this issue to find a proper solution in the long run.

tujoworker commented 2 weeks ago

I want to both add type and requirement via addIssue but get this type error: type' does not exist in type 'IssueInfo$1'. Any thoughts?

Here in this playground you see that requirement is undefined. My custom type I also need to know, in order to set the translation via setGlobalMessage. I want to set all the translations in "one" place, and not here and there.

With this custom "custom" schema, I get what I need. But it would be nicer to have less code to make that happen.

tujoworker commented 2 weeks ago

That said – I was a bit unclear regarding the other issue; when using setGlobalMessage, the issue.path is undefined. But it would help me a lot in order to show it in a certain translation message.

Here is a playground that shows this.

fabian-hiller commented 2 weeks ago

Here in this playground you see that requirement is undefined. My custom type I also need to know, in order to set the translation via setGlobalMessage.

My bad. At the moment it is not supported to set type and requirement via addIssue and I am not sure if we should add it since it is not intended that way. Also, rawCheck should only be used in special cases and not for simple validations.

I want to set all the translations in "one" place, and not here and there.

To create a custom t function is probably what you are looking for. The downside is that it is not tree-shakable.

import * as v from 'valibot';

type TranslationCode =
  | 'email:invalid'
  | 'email:format'
  | 'password:invalid'
  | 'password:length';

const translations: Record<string, Record<TranslationCode, string>> = {
  en: {
    'email:invalid': 'The email has the wrong data type',
    'email:format': 'The email is badly formatted',
    'password:invalid': 'The password has the wrong data type',
    'password:length': 'The password is too short',
  },
  de: {
    'email:invalid': 'Die E-Mail hat den falschen Datentyp',
    'email:format': 'Die E-Mail ist falsch formatiert',
    'password:invalid': 'Das Passwort hat den falschen Datentyp',
    'password:length': 'Das Passwort ist zu kurz',
  },
};

function t(code: TranslationCode): v.ErrorMessage<v.BaseIssue<unknown>> {
  return (issue) => translations[issue.lang || 'en']?.[code] ?? issue.message;
}

const Schema = v.object({
  email: v.pipe(v.string(t('email:invalid')), v.email(t('email:format'))),
  password: v.pipe(
    v.string(t('password:invalid')),
    v.minLength(8, t('password:length')),
  ),
});

That said – I was a bit unclear regarding the other issue; when using setGlobalMessage, the issue.path is undefined. But it would help me a lot in order to show it in a certain translation message.

Thanks for the tip. This is because each schema is independent and the path is added by the parent schema, but the error message is added before that.

tujoworker commented 1 week ago

The possibility to add translation directly in the schema is what I want to avoid. Because in my case, every "component" has its own little schema. But at that stage I don't want to deal with translations too. Because it makes the code unnecessary complex to read and maintain.

By dealing with translations later, I can handle it with one block of logic.

But so far, I can do that with my own "custom" schema function, like you have sees in one of the example codes.

So I think this issue can be closed.

fabian-hiller commented 1 week ago

Thanks for your feedback. I will close this issue for now, but if anyone has good ideas on how we can improve this part of the library in the long run, I will be happy to reopen it.