valibot: Getting concrete errors in unions is too hard

Hello, first of all, thank you for the work you are putting into this library. Currently I am trying to use it to validate an array of objects that can be of a variety of shapes, hence I use union to specify all the possible types.

When I get an error in a “first level” I get a quite readable message using my very basic “flatten function”. Something like: field name should not be empty at 4.name

However, when the error happens at a nested property, despite I am specifying proper specific errors for everything I get a very generic message: Invalid type at 4.input

I think the cause is the usage of union. Here is a basic example:

function nonEmptyString(name: string) {
    return string(`${name} should be a string`, [toTrimmed(), minLength(1, `${name} should not be empty`)])
}

const BasicInputSchema = object({ type: FieldTypeSchema });
const MultiSelectNotesSchema = object({
    type: literal("multiselect"), source: literal("notes"),
    folder: nonEmptyString('multi select source folder') // can't reach this message
});
const MultiSelectFixedSchema = object({ type: literal("multiselect"), source: literal("fixed"), multi_select_options: array(string()) });
export const MultiselectSchema = union([MultiSelectNotesSchema, MultiSelectFixedSchema]);

const InputSelectFixedSchema = object({
    type: literal("select"),
    source: literal("fixed"),
    options: array(object({
        value: nonEmptyString('Value of a select option'), // can't reach this message
        label: string()
    }))
});

const InputTypeSchema = union([
    BasicInputSchema,
    MultiselectSchema,
    InputSelectFixedSchema
]);

const FieldDefinitionSchema = object({
    name: nonEmptyString('field name'), // This is fine
    label: optional(string()),
    description: string(),
    input: InputTypeSchema
});

const FieldListSchema = array(FieldDefinitionSchema);

export function validateFields(fields: unknown) {
    const result = safeParse(FieldListSchema, fields);

    if (result.success) {
        return []
    }
    console.error('Fields issues', result.issues)
    return result.issues.map(issue =>
        `${issue.message} at ${issue.path?.map(item => item.key).join('.')}`
    );
}

I will be very grateful if you either provide a nicer way to reach this errors or a workaround to implement it myself.

Cheers

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Comments: 18 (8 by maintainers)

Commits related to this issue

Most upvoted comments

I have implemented discriminatedUnion. The API is expected to be available in the next release in a few days.

Yes, I see your point, and makes complete sense, I was thinking only in my particular use-case. What I think is needed in this case, it is a different combinator. Union, as you pointed out, can end having contradictory types, a tagged-union however should uniquely point you to one specific type through it’s tag. So my proposal is you add a new method, called however you want (io-ts calls it sum) where you define your tag key then you can just show the error of the type whose tag matches or just the sum error in case none matches:

sum('type', // This is the tag, all members must have it
    [object({type: 'A'}), object({type: 'B', something: string()}], // Sum of schemas
    'X does not match any valid input' // Fallback message
)

v0.20.0 is now available

Awesome, thanks for the update!

On Mon, Oct 23, 2023 at 9:49 PM Fabian Hiller @.***> wrote:

I have implemented discriminatedUnion. The API is expected to be available in the next release in a few days.

— Reply to this email directly, view it on GitHub https://github.com/fabian-hiller/valibot/issues/216#issuecomment-1775915043, or unsubscribe https://github.com/notifications/unsubscribe-auth/AARKJWPXUXNAEUEAPR7ZDETYA3C6BAVCNFSM6AAAAAA6H4LC2KVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTONZVHEYTKMBUGM . You are receiving this because you authored the thread.Message ID: @.***>

https://danielorodriguez.com

The reason I suspect the problem is using the union type is because

  1. the lack of proper information at the root
  2. the ridiculous and useless long issue.issues array that I don’t understand:
[
    {
        "reason": "type",
        "validation": "union",
        "origin": "value",
        "message": "Invalid type",
        "input": "dataview",
        "issues": [
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview"
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview"
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview"
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview"
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview"
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview"
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview"
            }
        ],
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "type",
                "value": "dataview"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "literal",
        "origin": "value",
        "message": "Invalid type",
        "input": "dataview",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "type",
                "value": "dataview"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "string",
        "origin": "value",
        "message": "folder name should be a string",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "folder"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "literal",
        "origin": "value",
        "message": "Invalid type",
        "input": "dataview",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "type",
                "value": "dataview"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "number",
        "origin": "value",
        "message": "Invalid type",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "min"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "number",
        "origin": "value",
        "message": "Invalid type",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "max"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "literal",
        "origin": "value",
        "message": "Invalid type",
        "input": "dataview",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "type",
                "value": "dataview"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "literal",
        "origin": "value",
        "message": "Invalid type",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "source"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "string",
        "origin": "value",
        "message": "folder name should be a string",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "folder"
            }
        ]
    },
    {
        "reason": "string",
        "validation": "min_length",
        "origin": "value",
        "message": "dataview query should not be empty",
        "input": "",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "query",
                "value": ""
            }
        ]
    },
    {
        "reason": "type",
        "validation": "literal",
        "origin": "value",
        "message": "Invalid type",
        "input": "dataview",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "type",
                "value": "dataview"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "literal",
        "origin": "value",
        "message": "Invalid type",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "source"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "array",
        "origin": "value",
        "message": "Invalid type",
        "path": [
            {
                "schema": "object",
                "input": {
                    "type": "dataview",
                    "query": "dv.pages('#person').map(p=>p.file.name)"
                },
                "key": "options"
            }
        ]
    },
    {
        "reason": "type",
        "validation": "union",
        "origin": "value",
        "message": "Invalid type",
        "input": {
            "type": "dataview",
            "query": "dv.pages('#person').map(p=>p.file.name)"
        },
        "issues": [
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview",
                "path": [
                    {
                        "schema": "object",
                        "input": {
                            "type": "dataview",
                            "query": "dv.pages('#person').map(p=>p.file.name)"
                        },
                        "key": "type",
                        "value": "dataview"
                    }
                ]
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "path": [
                    {
                        "schema": "object",
                        "input": {
                            "type": "dataview",
                            "query": "dv.pages('#person').map(p=>p.file.name)"
                        },
                        "key": "source"
                    }
                ]
            },
            {
                "reason": "type",
                "validation": "string",
                "origin": "value",
                "message": "multi select source folder should be a string",
                "path": [
                    {
                        "schema": "object",
                        "input": {
                            "type": "dataview",
                            "query": "dv.pages('#person').map(p=>p.file.name)"
                        },
                        "key": "folder"
                    }
                ]
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "input": "dataview",
                "path": [
                    {
                        "schema": "object",
                        "input": {
                            "type": "dataview",
                            "query": "dv.pages('#person').map(p=>p.file.name)"
                        },
                        "key": "type",
                        "value": "dataview"
                    }
                ]
            },
            {
                "reason": "type",
                "validation": "literal",
                "origin": "value",
                "message": "Invalid type",
                "path": [
                    {
                        "schema": "object",
                        "input": {
                            "type": "dataview",
                            "query": "dv.pages('#person').map(p=>p.file.name)"
                        },
                        "key": "source"
                    }
                ]
            },
            {
                "reason": "type",
                "validation": "array",
                "origin": "value",
                "message": "Invalid type",
                "path": [
                    {
                        "schema": "object",
                        "input": {
                            "type": "dataview",
                            "query": "dv.pages('#person').map(p=>p.file.name)"
                        },
                        "key": "multi_select_options"
                    }
                ]
            }
        ]
    }
]