fabian-hiller / valibot

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

How to pass down original input for use in config message #714

Closed smujaddid closed 1 month ago

smujaddid commented 1 month ago

Suppose I have 2 schemas for example (both not related)

const stringSchema = config(
  pipe(string(), maxLength(1)),
  { message: i => `${i.received} length is > 1` }
)

const integerSchema = config(
  pipe(transform(Number), number(), integer()),
  { message: i => `${i.received} is not a valid integer` },
)

in my unit tests I have these:

expect(() => parse(stringSchema, 'abcd')).toThrowWithMessage(Error, 'abcd length is > 1')
expect(() => parse(integerSchema, '27i9')).toThrowWithMessage(Error, '"27i9" is not a valid integer')

which results:

Expected to throw:
      [Error: abcd length is > 1]
Thrown:
      [ValiError: 4 length is > 1]

Expected to throw:
      [Error: "27i9" is not a valid integer]
Thrown:
      [ValiError: NaN is not a valid integer]

as you can see, the error message generated with config method, did used the current input for that action. For example, the 4 length is > 1 has the issue.received value from last action (maxLength) which is 4 that is the length of original input. Same goes for NaN is not valid, the result of transform(Number) get passed into number(). Both cases actually correct. But the problem is: How can I make the config use original input to generate same error message for the entire pipeline?

I'm expecting I can use the config to generate a message like this:

[ValiError: abcd length is > 1]
[ValiError: "27i9" is not a valid integer]

I already read through documentation but can't find anything to solve this.

fabian-hiller commented 1 month ago

You can use .input instead of .received for pipelines without transformations. I think the same only works for transformations when writing a custom schema: https://valibot.dev/api/custom/

import * as v from 'valibot';

const StringSchema = v.config(
  v.pipe(v.string(), v.maxLength(1)),
  { message: (issue) => `${v._stringify(issue.input)} length is > 1` } // or just `${issue.input}...`
);

const IntegerSchema = v.custom(
  (input) => Number.isInteger(Number(input)),
  (issue) => `${issue.received} is not a valid integer`
);
smujaddid commented 1 month ago

@fabian-hiller thanks! That's what I'm looking for.

I suggest to include this information on config and custom api documentation.

BTW, why the need to call v._stringify?

When I tried to do unit testing for above scenario, I have to wrap the input value with double quote in the toThrowWithMessage call just to pass assertion. I was expecting an unquoted string in that case. But in some scenarios, the string is not double quoted in default generated message.

smujaddid commented 1 month ago

@fabian-hiller While the .input solution works well for stringSchema in the example above, it doesn't work for integerSchema case because of transform action. The transform(Number) action changed both .input and .received value received by number() to transformed value which is NaN in this case, where I expected it to be same as original input 27i9.

fabian-hiller commented 1 month ago

This is exactly what I have described here:

You can use .input instead of .received for pipelines without transformations. I think the same only works for transformations when writing a custom schema: https://valibot.dev/api/custom/

This is implemented this way by design. I do not expect to change it at this time.

.expected and .received is a language-neutral string that describes the data property that was expected/received. It follows a specific syntax. For example, strings are enclosed in double quotes. You can use _stringify to convert an input to this format.

I suggest to include this information on config and custom api documentation.

Feel free to create a PR.

smujaddid commented 1 month ago

I understand. As for now, I refactor my schema to suit my needs. Thanks for your help.