Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalid issue message when using union with another schema inside it #1035

Open
NumerDev opened this issue Jan 26, 2025 · 5 comments
Open

Invalid issue message when using union with another schema inside it #1035

NumerDev opened this issue Jan 26, 2025 · 5 comments
Assignees
Labels
question Further information is requested

Comments

@NumerDev
Copy link

Hey, I came across a problem with parsing a union of a number and another schema. I have a case where I get a specific number or object with values. So I created a subSchema and a union type:

const subSchema = v.object({
    a: v.number(), 
    b: v.pipe(v.number(), v.picklist([1, 2, 3, 4])),
})

const schema = v.object({
    value: v.union([v.pipe(v.number(), v.value(-1)), subSchema]),
})

And now, parsing the valid values works correctly.

const valid_1 = v.safeParse(schema, { value: -1 }) // success

const valid_2 = v.safeParse(schema, { value: { a: 1, b: 4 } }) // success

But when I parse an invalid value I expect a proper error message for both cases.

const invalid = v.safeParse(schema, { value: 2 })
console.log(invalid)
/*
{
  typed: true,
  success: false,
  output: { value: 2 },
  issues: [
    {
      kind: 'validation',
      type: 'value',
      input: 2,
      expected: '-1',
      received: '2',
      message: 'Invalid value: Expected -1 but received 2',
      requirement: -1,
      path: [Array],
      issues: undefined,
      lang: undefined,
      abortEarly: undefined,
      abortPipeEarly: undefined
    }
  ]
}
*/

Instead, when I provide object input for the value field, I get a generic error that contains another issue array with two issue objects - the first invalid and the second valid.

const invalid = v.safeParse(schema, { value: { a: 1, b: 5 } }) // Expected error from picklist for `b` field
console.log(invalid)
/*
{
    typed: false,
    success: false,
    output: { value: { a: 1, b: 5 } },
    issues: [
        {
        kind: 'schema',
        type: 'union',
        input: [Object],
        expected: '(number | Object)',
        received: 'Object',
        message: 'Invalid type: Expected (number | Object) but received Object',
        requirement: undefined,
        path: [Array],
        issues: [Array],
        lang: undefined,
        abortEarly: undefined,
        abortPipeEarly: undefined
        }
      ]
    }
*/

console.log(invalid.issues && invalid.issues[0].issues) 
/*
  [
    {
        kind: 'schema',
        type: 'number',
        input: { a: 1, b: 5 },
        expected: 'number',
        received: 'Object',
        message: 'Invalid type: Expected number but received Object',
        requirement: undefined,
        path: undefined,
        issues: undefined,
        lang: undefined,
        abortEarly: undefined,
        abortPipeEarly: undefined
    },
    {
        kind: 'schema',
        type: 'picklist',
        input: 5,
        expected: '(1 | 2 | 3 | 4)',
        received: '5',
        message: 'Invalid type: Expected (1 | 2 | 3 | 4) but received 5', // <= proper message for object
        requirement: undefined,
        path: [ [Object] ],
        issues: undefined,
        lang: undefined,
        abortEarly: undefined,
        abortPipeEarly: undefined
    }
  ]
*/

I have been using valibot for a relatively short time so if this is not a bug and there is some other way to create such a scheme I would appreciate your help. I haven't found a similar issue anywhere, and it seems to me that the error message is not quite returned correctly in this case.

@fabian-hiller
Copy link
Owner

fabian-hiller commented Jan 27, 2025

I think this fixes your problem. See this playground.

Reach out if you want me to explain what happens under the hood.

import * as v from 'valibot';

const SubSchema = v.object({
  a: v.number(),
  b: v.pipe(v.number(), v.picklist([1, 2, 3, 4])),
});

const Schema = v.object({
  value: v.union([v.literal(-1), SubSchema]),
});
``

@fabian-hiller fabian-hiller self-assigned this Jan 27, 2025
@fabian-hiller fabian-hiller added the question Further information is requested label Jan 27, 2025
@NumerDev
Copy link
Author

NumerDev commented Jan 27, 2025

Even with your example after passing the object as a value

const result = v.safeParse(Schema, { value: { a: 2, b: 6} });

I get an issue message:

Invalid type: Expected (-1 | Object) but received Object

instead of

Invalid type: Expected (1 | 2 | 3 | 4) but received 6

Your example with object as a input: playground

@fabian-hiller
Copy link
Owner

fabian-hiller commented Jan 27, 2025

union internally checks all it's options. If no option matches the input, it checks whether exactly one of them returns a typed output (i.e. the type of the input matches the type of the schema). If so, it returns its issues. Otherwise, it returns a general issue with each collected issue as a subissue. This is necessary because these subissues are very likely to contradict each other. This is documented here: https://valibot.dev/api/union/

You are currently using two schema functions inside pipe, which usually does not make sense. I recommend you take a look at our mental model guide: https://valibot.dev/guides/mental-model/

The problem with your current schema is that the property value.b must match 1 | 2 | 3 | 4 to be typed. By using a validation action instead of a schema function, you can avoid this. We will soon provide a values action with PR #919 to simplify this code. Here is another playground.

const SubSchema = v.object({
  a: v.number(),
  b: v.pipe(
    v.number(),
    v.check(
      (input) => [1, 2, 3, 4].includes(input),
      (issue) =>
        `Invalid input: Expected (1 | 2 | 3 | 4) but received ${issue.received}`,
    ),
  ),
});

const Schema = v.object({
  value: v.union([v.literal(-1), SubSchema]),
});

After PR #919 is merged you can change this code to:

import * as v from 'valibot';

const SubSchema = v.object({
  a: v.number(),
  b: v.pipe(v.number(), v.values([1, 2, 3, 4])),
});

const Schema = v.object({
  value: v.union([v.literal(-1), SubSchema]),
});

Depending on your use case you could also use a range validation instead:

import * as v from 'valibot';

const SubSchema = v.object({
  a: v.number(),
  b: v.pipe(v.number(), v.minValue(1), v.maxValue(4)),
});

const Schema = v.object({
  value: v.union([v.literal(-1), SubSchema]),
});

@NumerDev
Copy link
Author

Okay, now I understand the reason. For now, I will stick to the v.check() example you provided, as the range validation would not work for my case.

Thank you for your help and explanation.

@fabian-hiller
Copy link
Owner

I will try to merge PR #919 next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants