Validation result
What validate, parse and the other functions return: what the result consists of, what happens to the data and how errors are structured. Every example below is real tinyval output.
What the result consists of
validate and validateAsync return an object with four fields:
| Field | When data is valid | When there are errors |
|---|---|---|
valid | true | false |
data | Cleaned data | undefined |
errors | {} | The first error of each field: { 'items.0.qty': '...' } |
issues | [] | All errors in detail: code, path, params, text |
parse and parseAsync return data directly or throw ValidationError.
Data is valid — what's in data
data is not the input — it's what the schema produced. Example:
const Order = object({
name: pipe(string(), trim(), minLength(2)),
email: pipe(string(), trim(), toLowerCase(), email()),
age: optional(pipe(coerce.number(), min(18))),
comment: optional(string()),
middleName: optional(nullable(string())),
newsletter: optional(coerce.boolean(), false),
items: array(object({ title: string(), qty: pipe(coerce.number(), min(1)) })),
});
validate(Order, {
name: ' Ivan ',
email: ' IVAN@Mail.RU ',
age: '30',
comment: '',
middleName: null,
items: [{ title: 'Book', qty: '2' }],
isAdmin: true,
});{
"valid": true,
"data": {
"name": "Ivan",
"email": "ivan@mail.ru",
"age": 30,
"middleName": null,
"newsletter": false,
"items": [{ "title": "Book", "qty": 2 }]
},
"errors": {},
"issues": []
}| Field | Input | In data | Why |
|---|---|---|---|
name | ' Ivan ' | 'Ivan' | trim |
email | ' IVAN@Mail.RU ' | 'ivan@mail.ru' | trim + toLowerCase |
age | '30' | 30 | coerce.number — the string became a number |
comment | '' | no field | An empty string in optional means "not provided", the field isn't added to data |
middleName | null | null | nullable keeps null |
newsletter | missing | false | The default from optional(…, false) |
items[0].qty | '2' | 2 | coerce.number inside the array |
isAdmin | true | no field | The unknown key is removed (object in strip mode) |
So data can go straight to the database: types are converted, nothing extra, and its TypeScript type is inferred from the schema.
There are errors — errors and issues
validate(Order, { name: 'I', email: 'bad', age: '15', items: [{ title: 'Book', qty: '0' }, { qty: '1' }] });{
"valid": false,
"data": undefined,
"errors": {
"name": "Must be at least 2 characters long",
"email": "Invalid email",
"age": "Must be at least 18",
"items.0.qty": "Must be at least 1",
"items.1.title": "Required"
},
"issues": [
{ "code": "minLength", "path": ["name"], "params": { "min": 2 }, "message": "Must be at least 2 characters long" },
{ "code": "email", "path": ["email"], "params": {}, "message": "Invalid email" },
{ "code": "min", "path": ["age"], "params": { "min": 18 }, "message": "Must be at least 18" },
{ "code": "min", "path": ["items", 0, "qty"], "params": { "min": 1 }, "message": "Must be at least 1" },
{ "code": "required", "path": ["items", 1, "title"], "params": {}, "message": "Required" }
]
}errors
An object "path → first error text". Handy to show an error next to its field: errors.email, errors['items.0.qty'].
- The path is joined with dots, array indexes are numbers:
items.1.title. - An error of the value itself (the schema isn't an object) is under the
''key:validate(string(), 42).errors→{ '': 'Must be a string' }. - One error per field — the first one in the order of rules in
pipe.
issues
An array of all errors in the order of schema fields. Each has:
| Field | Example | Why |
|---|---|---|
code | 'minLength' | The error code — for your own logic and translation. All codes are in Messages |
path | ['items', 0, 'qty'] | The path as an array: indexes are numbers, handy for nested forms |
params | { min: 1 } | Rule parameters — to build your own text on the client |
message | 'Must be at least 1' | Ready text, with language, label and custom messages applied |
All errors of a field and the Laravel format
By default a field has one error. With the { all: true } option issues gets all of them, and flatten groups them by field:
const r = validate(object({ password: pipe(string(), minLength(8), email()) }), { password: 'abc' }, { all: true });
r.errors; // { password: 'Must be at least 8 characters long' }
flatten(r.issues); // { password: ['Must be at least 8 characters long', 'Invalid email'] }parse and ValidationError
On error parse throws ValidationError — it has the same errors and issues, and message is joined into one line for logs:
try {
parse(Order, { name: 'I' });
} catch (e) {
e instanceof ValidationError; // true
e.message; // 'name: Must be at least 2 characters long; email: Required; items: Required'
e.errors; // { name: 'Must be at least 2 characters long', email: 'Required', items: 'Required' }
e.issues; // [{ code, path, params, message }, …]
}Standard Schema
Through schema['~standard'].validate the result follows the standard — { value } or { issues } with only message and path:
{
"issues": [
{ "message": "Must be at least 2 characters long", "path": ["name"] },
{ "message": "Required", "path": ["email"] },
{ "message": "Required", "path": ["items"] }
]
}Result type in TypeScript
Result<T> is a union: after checking valid, TypeScript knows whether data exists. More in TypeScript.
const r = validate(Order, input);
if (r.valid) r.data.items[0].qty; // number
else r.errors; // Record<string, string>See also
- validate, parse, flatten
- Empty values — why
''doesn't get intodata - Messages & i18n — the texts in
message