Skip to content

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:

FieldWhen data is validWhen there are errors
validtruefalse
dataCleaned dataundefined
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:

ts
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,
});
json
{
  "valid": true,
  "data": {
    "name": "Ivan",
    "email": "ivan@mail.ru",
    "age": 30,
    "middleName": null,
    "newsletter": false,
    "items": [{ "title": "Book", "qty": 2 }]
  },
  "errors": {},
  "issues": []
}
FieldInputIn dataWhy
name' Ivan ''Ivan'trim
email' IVAN@Mail.RU ''ivan@mail.ru'trim + toLowerCase
age'30'30coerce.number — the string became a number
comment''no fieldAn empty string in optional means "not provided", the field isn't added to data
middleNamenullnullnullable keeps null
newslettermissingfalseThe default from optional(…, false)
items[0].qty'2'2coerce.number inside the array
isAdmintrueno fieldThe 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 ​

ts
validate(Order, { name: 'I', email: 'bad', age: '15', items: [{ title: 'Book', qty: '0' }, { qty: '1' }] });
js
{
  "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:

FieldExampleWhy
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:

ts
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:

ts
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:

json
{
  "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.

ts
const r = validate(Order, input);
if (r.valid) r.data.items[0].qty; // number
else r.errors;                    // Record<string, string>

See also ​