--- url: https://js.livr-spec.org/guide/getting-started.md description: >- Install livr, create a validator, read the errors, and get the TypeScript type of the validated data from the schema. --- # Getting started ## Installation ```bash npm install livr ``` `livr` has no dependencies and ships its source as it is, with TypeScript declarations included. ### Browser LIVR works in browsers through any bundler (Vite, esbuild, Rollup, webpack). Without a build step, load it from an ESM CDN: ```html ``` ## First validator ```js import LIVR from 'livr'; const validator = new LIVR.Validator({ name: 'required', email: ['required', 'email'], age: 'positive_integer', password: ['required', { min_length: 8 }], password2: { equal_to_field: 'password' }, }); const validData = validator.validate(userData); if (validData) { // Validated and cleaned data: fields without rules are removed saveUser(validData); } else { console.log(validator.getErrors()); // { email: 'WRONG_EMAIL', password: 'TOO_SHORT' } } ``` A schema maps field names to rules. A field can have one rule (`'required'`), a list of rules (`['required', 'email']`), and a rule can take arguments (`{ min_length: 8 }`). The schema is plain data, so it can just as well come from a JSON file or a database. Rule names work in both `snake_case` and `camelCase`: `one_of` and `oneOf`, `min_length` and `minLength` are the same rule. The [LIVR specification](https://livr-spec.org) uses `snake_case`, so schemas written that way also work in the other implementations. `validate()` returns the validated data, or `false` if the data is invalid. `getErrors()` then returns error codes shaped like the input. Empty values (`undefined`, `null` and `''`) pass every rule except `required`, so optional fields need no special handling. ## With TypeScript Add `as const` to the schema and get the type of the validated data from it: ```ts import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; const userSchema = { name: ['required', 'string'], email: ['required', 'email'], age: 'positive_integer', role: { one_of: ['admin', 'user'] }, } as const; type User = InferFromSchema; // { name: string; email: string; age?: number; role?: 'admin' | 'user' } const validator = new LIVR.Validator(userSchema); const validData = validator.validate(input); if (validData) { console.log(validData.age); // number | undefined } ``` In a new project, also turn on [strict mode](/typescript.md#strict-mode): a misspelled rule or a rule without a type is a compile error then, instead of the `unknown` type. Add this to any `.ts` file of the project: ```ts // The import makes the file a module and loads the types of livr import type {} from 'livr/types/inference'; declare module 'livr/types/inference' { interface TypeOptions { strictRules: true; } } ``` Nested objects, lists, unions and types of your own rules are covered in the [TypeScript guide](/typescript.md). ## Next steps * [Usage](./usage.md): modifiers, async validation, auto-trim and smaller bundles * [Custom rules](./custom-rules.md): aliases and rules of your own * [Built-in rules](../rules.md): every rule with a link to its specification * [Playground](https://livr-spec.org/playground.html): try schemas in the browser --- --- url: https://js.livr-spec.org/guide/usage.md description: >- Validating data, error codes, modifiers, auto-trim, async validation with AsyncValidator, smaller bundles and performance tips. --- # Usage ## Basic validation ```js import LIVR from 'livr'; const validator = new LIVR.Validator({ name: 'required', email: ['required', 'email'], gender: { one_of: ['male', 'female'] }, phone: { max_length: 10 }, password: ['required', { min_length: 10 }], password2: { equal_to_field: 'password' }, }); const validData = validator.validate(userData); if (validData) { saveUser(validData); } else { console.log(validator.getErrors()); } ``` Construct a validator once and use it for many inputs: rules are compiled on the first validation, and every later `validate()` call only runs them. Call `prepare()` to compile them in advance. ## Errors Errors are codes, not messages, and they mirror the structure of the input: ```js { email: 'WRONG_EMAIL', password: 'TOO_SHORT', address: { zip: 'NOT_POSITIVE_INTEGER' }, tags: [null, 'TOO_LONG'], } ``` All errors are returned at once. Translate the codes in your UI and attach them to form fields by path. The [error codes](https://livr-spec.org/validation-rules/error-codes.html) are the same in every LIVR implementation. ## Modifiers Modifiers change the output while it is validated: ```js const validator = new LIVR.Validator({ email: ['required', 'trim', 'email', 'to_lc'], // trim, validate, lowercase age: ['positive_integer', { default: 18 }], // a default for an empty value }); ``` Available modifiers: `trim`, `to_lc`, `to_uc`, `default`, `remove`, `leave_only`. Numeric rules also convert valid strings to numbers, so `'42'` from a form becomes `42` in the output. ## Auto-trim Trim every string before validation, for one validator or for all of them: ```js const validator = new LIVR.Validator(schema, { autoTrim: true }); LIVR.Validator.defaultAutoTrim(true); // all validators created after this call ``` ## Async validation For rules that query a database or call an API: ```js import LIVR from 'livr/async'; const validator = new LIVR.AsyncValidator({ username: ['required', 'unique_username'], // a custom async rule email: ['required', 'email'], }); try { const validData = await validator.validate(userData); saveUser(validData); } catch (errors) { console.log(errors); } ``` Differences from the sync validator: * Import from `'livr/async'` and use `AsyncValidator` * `validate()` returns a promise, and rejects with the errors object (there is no `getErrors()`) * Fields are validated in parallel, the rules of one field run one after another * Sync rules stay sync: a field waits for a promise only when one of its rules returns one ## Smaller bundles `livr` registers all built-in rules. To ship only the rules you use, start from the bare validator: ```js import Validator from 'livr/lib/Validator'; import required from 'livr/lib/rules/common/required'; import email from 'livr/lib/rules/special/email'; import min_length from 'livr/lib/rules/string/min_length'; import equal_to_field from 'livr/lib/rules/special/equal_to_field'; Validator.registerDefaultRules({ required, email, min_length, equal_to_field }); const validator = new Validator({ email: ['required', 'email'], password: ['required', { min_length: 8 }], password2: { equal_to_field: 'password' }, }); ``` The validator core is about 2 KB min+gzip. See [Zero dependencies](https://js.livr-spec.org/why/zero-dependencies) for the sizes. ## Performance tips ```js // Good: create once, validate many times const validator = new LIVR.Validator(schema); for (const item of items) { const result = validator.validate(item); } // Slower: a new validator for every item for (const item of items) { const validator = new LIVR.Validator(schema); const result = validator.validate(item); } ``` Creating validators is cheap too: LIVR is the fastest in the benchmark that creates a validator for every call. Still, a reused validator is 2–3 times faster. See [Performance](https://js.livr-spec.org/why/performance). --- --- url: https://js.livr-spec.org/guide/custom-rules.md description: >- Your own rules: aliases of existing rules, rule functions, async rules, registering rules for all validators, and the TypeScript types of custom rules. --- # Custom rules A custom rule is an alias of existing rules or a rule function. Register it for one validator or for all of them. With TypeScript, also [declare its type](#types-of-custom-rules): registering a rule does not give it a type. ## Aliases (recommended) Combine existing rules into a rule of your domain, with its own error code: ```js const validator = new LIVR.Validator({ password: ['required', 'strong_password'], age: ['required', 'adult_age'], }); validator.registerAliasedRule({ name: 'strong_password', rules: { min_length: 8 }, error: 'WEAK_PASSWORD', }); validator.registerAliasedRule({ name: 'adult_age', rules: ['positive_integer', { min_number: 18 }], error: 'MUST_BE_ADULT', }); ``` An alias is data too: its rules are a regular LIVR schema, so aliases can be loaded from a config file or a database, and they work the same in every LIVR implementation. ## Rule functions A rule is a factory: it receives the rule arguments and returns a function that checks one value. The function returns an error code on failure and nothing on success: ```js const validator = new LIVR.Validator({ password: ['required', 'strong_password'], }); validator.registerRules({ strong_password() { return value => { // Empty values are the job of 'required' if (value === undefined || value === null || value === '') return; if (!/[A-Z]/.test(value)) return 'MISSING_UPPERCASE'; if (!/[a-z]/.test(value)) return 'MISSING_LOWERCASE'; if (!/[0-9]/.test(value)) return 'MISSING_DIGIT'; if (value.length < 8) return 'TOO_SHORT'; }; }, }); ``` The function gets three arguments: `(value, allValues, outputArr)`. `allValues` is the whole input object, for rules that compare fields. To change the output, push the new value to `outputArr`: ```js validator.registerRules({ to_date() { return (value, allValues, outputArr) => { if (value === undefined || value === null || value === '') return; const date = new Date(value); if (Number.isNaN(date.getTime())) return 'WRONG_DATE'; outputArr.push(date); }; }, }); ``` The factory runs once, when the validator compiles its rules, so do the expensive work there (for example, build a regular expression from the arguments) and keep the returned function small. ## Async rules With `AsyncValidator`, a rule can return a promise: ```js import LIVR from 'livr/async'; const validator = new LIVR.AsyncValidator({ username: ['required', 'unique_username'], }); validator.registerRules({ unique_username() { return async value => { if (value === undefined || value === null || value === '') return; const exists = await db.users.exists({ username: value }); if (exists) return 'USERNAME_TAKEN'; }; }, }); ``` ## Registering rules globally Rules and aliases registered on the class are available to every validator created afterwards, sync and async: ```js LIVR.Validator.registerDefaultRules({ my_rule(arg1, arg2) { return (value, allValues, outputArr) => { if (invalid) return 'ERROR_CODE'; }; }, }); LIVR.Validator.registerAliasedDefaultRule({ name: 'valid_address', rules: { nested_object: { country: 'required', city: 'required' } }, }); ``` ## Types of custom rules Registering a rule does not give it a type. To get type inference for your own rules and aliases, declare their types in `RuleTypes`: the type of the value after the rule. Declare it once, with the `snake_case` name, and the `camelCase` name gets it too: ```ts import LIVR from 'livr'; import type { As, AliasType } from 'livr/types/inference'; const phoneRules = ['string', { like: '^\\+?[0-9]{10,15}$' }] as const; LIVR.Validator.registerAliasedDefaultRule({ name: 'phone', rules: phoneRules }); declare module 'livr/types/inference' { interface RuleTypes { my_rule: As; // checks a string to_date: Date; // converts the value to a Date phone: AliasType; } } ``` The declaration can be in any `.ts` file of the project that has an `import` or an `export`, for example next to the code that registers the rules. Without them, the file declares a new module instead of adding to livr. The order of the declaration and the registration does not matter. See the types in the [TypeScript Playground](https://www.typescriptlang.org/play/?#code/PTAEBUAsFNQBQDYEMCeBzATgewK4DsATUAYxmIGsBnUAFxlpQAdpqs8EUAuUASxuoBK+UACMcNGmxJI8eLDVAIsSInkYBbUIyQUkaFop7lYCHgDcMAKB7rGWDAoAyASQBqA0ADNsmgOSmLXwBua1t7BRomWABvUGc8T2gMADEfAGVSaHUkABpQF3cMmGzQAF8vH1B-cwxgSOZKYNC7BwZmUFiAQUo8ztMkSnAosoqsPwDa+pZgHgSk6DxiaCbLEFBOvFAkfspuaAAPHkoaWbRQDBwEA3wCJK3QPGgAdwekdWg8p75ILc2k7AwJCwt1AWE8vH4oKeeEsxDYxy0kDY0CEV2oAF5QABtXzHDCnXx5WKmYzcXwAPQAOpSANQAfixAAYALQATgAutEAIyMnJcgCspQAJL4yuyttQ4XhjiECgIAHSubY8AhISQYeUYaBoI40JJ9HgDaAEAAi0E8SEuNFR0AAFLE8G9oGTGEjHoTzpcWNxXcibT1QP97GSAOoCADyADkAOIAfTgAAkowBRUWlACUIVWYE6nquXnwxBObG4sWOSAclFjXzoZLSAGkAKrMtPSBAIajlyvV7623wN5u+dOgxYfbO-Ih0WCeQvFzZ8c7QGg4DDSkhkKiBnQ-MzbHDQSxyxXK1XqzXa3VJM0Wq3++2WUCgLv8Ht0W2MLWeHj7bh407D6IH0fRdl1XUBbV3BB924fByDkaFh3RAA+DogOA3hwVtI9xB4BB5SOSMsCVKC7Ug-d02HLVQM2G5zVmY0glANYskYSJQDIgwK1gKdQAAKywERQXBXwtQARxwHgtQIXw0OAnhMKmMF2L3WAAEJ0UxXEaHxPA0FFAAffTQFUjj5WfSgQ17D86P2CiQJXTZfDDKM4zgARk2SZwAA0mnQx8qIc0BaK-R4CBCdDSnCsocksDMszWAQL2OJJTnuC58wILADDkBQdTMWAFyQNpnVAW5iGQLVaHoKZqFmUAbSGBp5XHNJtJ4ItQHUYFYGyYxqCK9QjkoZh22NPNYHsNKvVAGskXEe4pnuOFbFw2AgysMqKp64FpuqCw6iiSgZjmLVR1FQDH1mPUMAtJZ6q9RqDAu4DfUebgDQGR6AB5FPBV6US9SgkKix9zNfSB3soL6-104GgNKSwgKupJbtgR7w1Ynh4VQ9C-yLf1uG0-cooRhHYXhBRKEyEpMWe-7uBxMSJKkj1fH+3x2Ri0HyBwUsnxoCsX1musmxbaKgOY7JcLJLJDQQD01kLMZ3jwGhuCK5bGFWwMMABXhNjxhQutuWKJSBaVKbVI4vwMOUiiyJB4rABMsHywE4GwAgcA6yQn2gbj6FmRJdbGqZLEWj2do6zF4mD1Ixnt7IfqiJSqeKJA4fJi3lNMU9JsxR4XiPYiVTVewvsjr2iyQ2004dzMs4RD8o4UTFINLs929PO1YnpqoaQAZgADkZflGS5AAmAeABZ+QANgAdg9SgeZF5tp4ntMG+sTDm6rmgAKAvfvZoMyecYtYYbOIzgvoghTegDsYiAqVKCwK55SUNAIJPMuNX0GgyZdb2EoLaCiIQEZAA): an alias and a rule function with their types, in strict mode. The type of a rule is one of: * `As`: the rule checks that the value is a `T`, or converts it to `T`; * the type itself, when the rule makes a new value, like `to_date: Date`; * `AliasType` for an alias, with its rules in a constant `as const`; * an interface that extends `RuleType`, when the type depends on the rule arguments or on earlier rules: `{ currency: ['USD', 'EUR'] }` gives `'USD' | 'EUR'`. See [Types from arguments and earlier rules](/typescript.md#types-from-arguments-and-earlier-rules). Rules that make the field required, meta rules, replaced built-in rules and rule packages are covered in [Custom rules type inference](/typescript.md#custom-rules-type-inference). A rule without a type gives `unknown`. To make it a compile error, for misspelled rules and forgotten types, turn on [strict mode](/typescript.md#strict-mode), as new projects should. ## More rules [livr-extra-rules](https://www.npmjs.com/package/livr-extra-rules) adds rules such as `ipv4` and `md5`. `boolean`, `credit_card`, `uuid`, `iso_date` with ranges and most of its other rules are built in now, as [LIVR 2.5 rules](../rules.md#livr-25-rules). --- --- url: https://js.livr-spec.org/rules.md description: >- The built-in rules of the LIVR specification with the TypeScript type of each rule. --- # Built-in rules `livr` implements LIVR 2.0 and LIVR 2.5: every rule of the [LIVR specification](https://livr-spec.org/validation-rules.html) is built in. This page is a short list with the TypeScript type of every rule, in the groups of the specification. The full reference, with the arguments, error codes and examples of every rule, is on the specification site: Rule names below link to their pages in the specification. The last column shows the TypeScript type that [`InferFromSchema`](/typescript.md) gives a field with this rule. Rules that check a value keep a more precise type of earlier rules: `[{ one_of: ['a', 'b'] }, { max_length: 1 }]` is `'a' | 'b'`, not `string`. Rule names work in `snake_case` and in `camelCase`: `min_length` and `minLength` are the same rule. Rules marked 2.5 were added in LIVR 2.5, see [below](#livr-25-rules) what they have in common. ## Common rules | Rule | Checks | Type | | --- | --- | --- | | [`required`](https://livr-spec.org/validation-rules/required.html) | The value is present: not missing, not `null` and not an empty string. `0` and `false` are valid | the type of earlier rules, field becomes required | | [`not_empty`](https://livr-spec.org/validation-rules/not_empty.html) | The value is not an empty string, if the field is passed at all | `string`, or the type of earlier rules | | [`not_empty_list`](https://livr-spec.org/validation-rules/not_empty_list.html) | The value is a list with at least one element | `unknown[]`, or the list type of earlier rules, field becomes required | | [`any_object`](https://livr-spec.org/validation-rules/any_object.html) | The value is a plain object, its content is not validated | `Record` | | [`required_if`](https://livr-spec.org/validation-rules/required_if.html)2.5 | The field is required when other fields have the given values: `{ required_if: { type: 'company' } }` | the type of earlier rules | | [`any`](https://livr-spec.org/validation-rules/any.html)2.5 | Any value passes, unchanged: a rule for a field that is kept without checks | `unknown`, or the type of earlier rules | | [`boolean`](https://livr-spec.org/validation-rules/boolean.html)2.5 | `true`, `false`, `1`, `0`, `"1"`, `"0"`, `"true"` or `"false"`, returned as a boolean | `boolean` | ## String rules | Rule | Checks | Type | | --- | --- | --- | | [`string`](https://livr-spec.org/validation-rules/string.html) | The value is a primitive, returned as a string | `string` | | [`eq`](https://livr-spec.org/validation-rules/eq.html) | The value equals the given one | the literal | | [`one_of`](https://livr-spec.org/validation-rules/one_of.html) | The value is one of the allowed values | a union of the literals | | [`max_length`](https://livr-spec.org/validation-rules/max_length.html) | The string is not longer than the given length | `string` | | [`min_length`](https://livr-spec.org/validation-rules/min_length.html) | The string is at least the given length | `string` | | [`length_between`](https://livr-spec.org/validation-rules/length_between.html) | The length is between two values, inclusive | `string` | | [`length_equal`](https://livr-spec.org/validation-rules/length_equal.html) | The string has exactly the given length | `string` | | [`like`](https://livr-spec.org/validation-rules/like.html) | The value matches a regular expression | `string` | | [`not_one_of`](https://livr-spec.org/validation-rules/not_one_of.html)2.5 | The value is none of the given values | `string \| number \| boolean`, or the type of earlier rules | | [`digits`](https://livr-spec.org/validation-rules/digits.html)2.5 | The string has only the digits 0-9 | `string` | | [`starts_with`](https://livr-spec.org/validation-rules/starts_with.html)2.5, [`ends_with`](https://livr-spec.org/validation-rules/ends_with.html)2.5, [`contains`](https://livr-spec.org/validation-rules/contains.html)2.5 | The string starts with, ends with or contains one of the given strings | `string` | ## Numeric rules Numeric rules accept strings that look like numbers, such as `'42'` from a form, and return numbers. | Rule | Checks | Type | | --- | --- | --- | | [`integer`](https://livr-spec.org/validation-rules/integer.html) | The value is an integer | `number` | | [`positive_integer`](https://livr-spec.org/validation-rules/positive_integer.html) | The value is an integer greater than zero | `number` | | [`decimal`](https://livr-spec.org/validation-rules/decimal.html) | The value is a number, an integer or a decimal | `number` | | [`positive_decimal`](https://livr-spec.org/validation-rules/positive_decimal.html) | The value is a number greater than zero | `number` | | [`max_number`](https://livr-spec.org/validation-rules/max_number.html) | The number is not greater than the given value | `number` | | [`min_number`](https://livr-spec.org/validation-rules/min_number.html) | The number is not less than the given value | `number` | | [`number_between`](https://livr-spec.org/validation-rules/number_between.html) | The number is between two values, inclusive | `number` | | [`max_decimals`](https://livr-spec.org/validation-rules/max_decimals.html)2.5 | The number has at most the given count of decimal places | `number` | ## Special rules | Rule | Checks | Type | | --- | --- | --- | | [`email`](https://livr-spec.org/validation-rules/email.html) | The value is an email address | `string` | | [`url`](https://livr-spec.org/validation-rules/url.html) | The value is an `http` or `https` URL | `string` | | [`iso_date`](https://livr-spec.org/validation-rules/iso_date.html) | The value is an ISO 8601 date without time, like `2014-08-14`. LIVR 2.5 adds bounds2.5: `{ iso_date: { min, max } }` with dates, `today`, `yesterday` or `tomorrow` (days in UTC). The 2.0 forms are unchanged | `string` | | [`iso_datetime`](https://livr-spec.org/validation-rules/iso_datetime.html)2.5 | An ISO 8601 date and time with an offset, returned as the instant in UTC, like `2026-01-02T01:04:05.000Z`. Also with `{ min, max }` | `string` | | [`iso_time`](https://livr-spec.org/validation-rules/iso_time.html)2.5 | A time `HH:MM`, `HH:MM:SS` or with a fraction, without an offset. Also with `{ min, max }`, a range over midnight when `min` is after `max` | `string` | | [`equal_to_field`](https://livr-spec.org/validation-rules/equal_to_field.html) | The value equals another field, like a password confirmation | `string`, or the type of earlier rules | ## Metarules Metarules describe nested data with other rules. | Rule | Describes | Type | | --- | --- | --- | | [`nested_object`](https://livr-spec.org/validation-rules/nested_object.html) | A nested object | the object type | | [`variable_object`](https://livr-spec.org/validation-rules/variable_object.html) | An object whose rules depend on a selector field | a union of the object types | | [`list_of`](https://livr-spec.org/validation-rules/list_of.html) | A list, the rules apply to every element | an array | | [`list_of_objects`](https://livr-spec.org/validation-rules/list_of_objects.html) | A list of objects | an array of the object type | | [`list_of_different_objects`](https://livr-spec.org/validation-rules/list_of_different_objects.html) | A list of objects whose rules depend on a selector field | an array of the union | | [`or`](https://livr-spec.org/validation-rules/or.html) | Alternatives: the first set of rules that succeeds wins | a union | | [`when`](https://livr-spec.org/validation-rules/when.html)2.5 | Rules that depend on another field: `{ when: ['type', { eq: 'company' }, 'required'] }`, with rules for the other case, or cases by value | a union of the types of the branches | | [`values`](https://livr-spec.org/validation-rules/values.html)2.5 | An object, the rules apply to every value: `{ values: 'integer' }` | `Record` | | [`keys`](https://livr-spec.org/validation-rules/keys.html)2.5 | An object, the rules check every key: `{ keys: { one_of: ['en', 'uk'] } }` | the object type of earlier rules | ## List rules All list rules were added in LIVR 2.5. | Rule | Checks | Type | | --- | --- | --- | | [`list_length`](https://livr-spec.org/validation-rules/list_length.html) | The list has the given length, or a length between `min` and `max` | the list type of earlier rules | | [`unique`](https://livr-spec.org/validation-rules/unique.html) | The items of the list are unique, or the values of the given keys of its objects | the list type of earlier rules | ## Format rules All format rules were added in LIVR 2.5. | Rule | Checks | Type | | --- | --- | --- | | [`uuid`](https://livr-spec.org/validation-rules/uuid.html) | A UUID (RFC 9562), of the given versions `'v1'`-`'v8'` | `string` | | [`ip`](https://livr-spec.org/validation-rules/ip.html), [`cidr`](https://livr-spec.org/validation-rules/cidr.html) | An IPv4 or IPv6 address, or a network in CIDR notation, `'v4'` or `'v6'` only | `string` | | [`hostname`](https://livr-spec.org/validation-rules/hostname.html) | A DNS host name | `string` | | [`base64`](https://livr-spec.org/validation-rules/base64.html) | Base64, `'url'` for the URL-safe alphabet | `string` | | [`hex`](https://livr-spec.org/validation-rules/hex.html), [`digest`](https://livr-spec.org/validation-rules/digest.html) | Hex digits, or a hex digest of `md5`, `sha1`, `sha256`, `sha384` or `sha512` | `string` | | [`mongo_id`](https://livr-spec.org/validation-rules/mongo_id.html) | A MongoDB ObjectId, 24 hex digits | `string` | | [`hex_color`](https://livr-spec.org/validation-rules/hex_color.html) | A CSS hex color, like `#fff` or `#ffffff80` | `string` | | [`mac`](https://livr-spec.org/validation-rules/mac.html) | A MAC address, 48 bits or with `64` an EUI-64 | `string` | | [`e164`](https://livr-spec.org/validation-rules/e164.html) | A phone number in the E.164 format, like `+380441234567` | `string` | | [`semver`](https://livr-spec.org/validation-rules/semver.html) | A semantic version, like `1.2.3-beta.1` | `string` | | [`jwt`](https://livr-spec.org/validation-rules/jwt.html) | A JSON Web Token in the compact form. The signature is not checked | `string` | ## Checksum rules All checksum rules were added in LIVR 2.5. They check the value as it is: remove spaces and dashes before them, with `remove`. | Rule | Checks | Type | | --- | --- | --- | | [`luhn`](https://livr-spec.org/validation-rules/luhn.html) | At least 2 digits with a valid Luhn checksum | `string` | | [`credit_card`](https://livr-spec.org/validation-rules/credit_card.html) | A payment card number: 12-19 digits with a valid Luhn checksum | `string` | | [`iban`](https://livr-spec.org/validation-rules/iban.html) | An IBAN with valid check digits | `string` | ## Modifiers Modifiers change the output and never return errors. | Rule | Does | Type | | --- | --- | --- | | [`trim`](https://livr-spec.org/validation-rules/trim.html) | Removes leading and trailing whitespace | `string` | | [`to_lc`](https://livr-spec.org/validation-rules/to_lc.html) | Converts to lower case | `string`, literal types in lower case | | [`to_uc`](https://livr-spec.org/validation-rules/to_uc.html) | Converts to upper case | `string`, literal types in upper case | | [`remove`](https://livr-spec.org/validation-rules/remove.html) | Removes the given characters | `string` | | [`leave_only`](https://livr-spec.org/validation-rules/leave_only.html) | Removes all characters except the given ones | `string` | | [`default`](https://livr-spec.org/validation-rules/default.html) | Sets a value when the value is empty | the type of the default, field becomes required | | [`split`](https://livr-spec.org/validation-rules/split.html)2.5 | Splits a string at the separator into a list of strings | `string[]` | ## About the LIVR 2.5 rules LIVR 2.5 added the rules marked 2.5, the list, format and checksum rules, and bounds for `iso_date`, see the [LIVR 2.5 specification](https://github.com/koorchik/LIVR/blob/master/drafts/v2.5.md). It only adds rules: every LIVR 2.0 schema gives the same result. Implementations of LIVR 2.0 in other languages do not have these rules. The rules are loaded on first use, so a schema without them costs nothing. A rule you register under one of these names replaces the built-in one, so schemas with `boolean`, `uuid` or `required_if` of livr-extra-rules keep working while the package is registered. Like the LIVR 2.0 rules, they skip a missing value, `null` and an empty string, so they work with `required`; only `required_if` checks them. A rule without arguments rejects any argument, `{ boolean: null }` included, and an unknown option is a schema error: a schema says what it checks. ## More rules [livr-extra-rules](https://www.npmjs.com/package/livr-extra-rules) adds more rules, like `ipv4` and `md5`; most of its other rules are built in now, as LIVR 2.5 rules. You can also [write your own](./guide/custom-rules.md). --- --- url: https://js.livr-spec.org/typescript.md --- # TypeScript Type Inference LIVR provides powerful TypeScript type inference that automatically derives types from your validation schemas. This gives you type safety without manually defining interfaces that duplicate your validation rules. See it without installing anything: [open an example in the TypeScript Playground](https://www.typescriptlang.org/play/?#code/PTAEBUAsFNQBQDYEMCeBzATgewK4DsATUAYxmIGsBnUAFxlpQAdpqs8EUAuUASxuoBK+UACMcNGmxJI8eLDVAIsSInkYBbUIyQUkaFop7lYCHgDcMAKB7rGWDAoAyASQBqA0ADNsmgOSmLXwBua1t7BRomWABvUGc8T2gMADEfAGVSaHUkUABfLx9Qf3MMYEjmSmDLSxBQAEFQF3dQSkzs3mpGZB48UAIkGiQAOlAAAyRqYjZKGlHFaH4IKIyMHkYFHsSMAqxNPkspvBmWtpyAXlBoy1BQLKQeBG4AbV8MaABHHB43gl8AGiKdwevgAun9rqBsAhoNxYmxoAB9LCeZ6+FTqHr-Io4ShJUF5cE3FQEN6USiwiE3PAsGjQAhIkQAK2gxBoFJuHJIfC4oBeb0+3zpWN8M1WeDQoMJnNAAC81txfHZKHxzIierT9Bh-pSCRDclLBmhyZdDDMkSiiqKehLdfkJiRpjQQjUwAAJLBmJKgACquO2kha0FgdFgmySPwYzEs5Vgvq9F3iW1SuwyMGyAB4Y8iTmmkAA+Z2HY5mJCmfqSbYXakAd0abgEQ1cpZ45fs6bjGDzAApWrmAJQhIsKHHx0AlssDexDcctgbQLuxIGPIoyPAAAWgAA8kLZoUMpuosVCYSuCBi8FjDcaXozKiC8gPqjxPKAuyOMH3LhD30NiaTKAA-Pu3JBKAtRWuKoAAD6gPgBDQJ4PR0pY+TQAguJfjcRZYHuShoF2M6thgQz6DQACiGDYBglBdn2j65JYQA) and hover over the types. Custom rules with their types, in strict mode: [another example](https://www.typescriptlang.org/play/?#code/PTAEBUAsFNQBQDYEMCeBzATgewK4DsATUAYxmIGsBnUAFxlpQAdpqs8EUAuUASxuoBK+UACMcNGmxJI8eLDVAIsSInkYBbUIyQUkaFop7lYCHgDcMAKB7rGWDAoAyASQBqA0ADNsmgOSmLXwBua1t7BRomWABvUGc8T2gMADEfAGVSaHUkABpQF3cMmGzQAF8vH1B-cwxgSOZKYNC7BwZmUFiAQUo8ztMkSnAosoqsPwDa+pZgHgSk6DxiaCbLEFBOvFAkfspuaAAPHkoaWbRQDBwEA3wCJK3QPGgAdwekdWg8p75ILc2k7AwJCwt1AWE8vH4oKeeEsxDYxy0kDY0CEV2oAF5QABtXzHDCnXx5WKmYzcXwAPQAOpSANQAfixAAYALQATgAutEAIyMnJcgCspQAJL4yuyttQ4XhjiECgIAHSubY8AhISQYeUYaBoI40JJ9HgDaAEAAi0E8SEuNFR0AAFLE8G9oGTGEjHoTzpcWNxXcibT1QP97GSAOoCADyADkAOIAfTgAAkowBRUWlACUIVWYE6nquXnwxBObG4sWOSAclFjXzoZLSAGkAKrMtPSBAIajlyvV7623wN5u+dOgxYfbO-Ih0WCeQvFzZ8c7QGg4DDSkhkKiBnQ-MzbHDQSxyxXK1XqzXa3VJM0Wq3++2WUCgLv8Ht0W2MLWeHj7bh407D6IH0fRdl1XUBbV3BB924fByDkaFh3RAA+DogOA3hwVtI9xB4BB5SOSMsCVKC7Ug-d02HLVQM2G5zVmY0glANYskYSJQDIgwK1gKdQAAKywERQXBXwtQARxwHgtQIXw0OAnhMKmMF2L3WAAEJ0UxXEaHxPA0FFAAffTQFUjj5WfSgQ17D86P2CiQJXTZfDDKM4zgARk2SZwAA0mnQx8qIc0BaK-R4CBCdDSnCsocksDMszWAQL2OJJTnuC58wILADDkBQdTMWAFyQNpnVAW5iGQLVaHoKZqFmUAbSGBp5XHNJtJ4ItQHUYFYGyYxqCK9QjkoZh22NPNYHsNKvVAGskXEe4pnuOFbFw2AgysMqKp64FpuqCw6iiSgZjmLVR1FQDH1mPUMAtJZ6q9RqDAu4DfUebgDQGR6AB5FPBV6US9SgkKix9zNfSB3soL6-104GgNKSwgKupJbtgR7w1Ynh4VQ9C-yLf1uG0-cooRhHYXhBRKEyEpMWe-7uBxMSJKkj1fH+3x2Ri0HyBwUsnxoCsX1musmxbaKgOY7JcLJLJDQQD01kLMZ3jwGhuCK5bGFWwMMABXhNjxhQutuWKJSBaVKbVI4vwMOUiiyJB4rABMsHywE4GwAgcA6yQn2gbj6FmRJdbGqZLEWj2do6zF4mD1Ixnt7IfqiJSqeKJA4fJi3lNMU9JsxR4XiPYiVTVewvsjr2iyQ2004dzMs4RD8o4UTFINLs929PO1YnpqoaQAZgADkZflGS5AAmAeABZ+QANgAdg9SgeZF5tp4ntMG+sTDm6rmgAKAvfvZoMyecYtYYbOIzgvoghTegDsYiAqVKCwK55SUNAIJPMuNX0GgyZdb2EoLaCiIQEZAA). ## Table of Contents * [Basic Usage](#basic-usage) * [Async Validator](#async-validator) * [How Types Are Inferred](#how-types-are-inferred) * [Required vs Optional Fields](#required-vs-optional-fields) * [Types of the Built-in Rules](#types-of-the-built-in-rules) * [Literal Types](#literal-types-with-one_of-and-eq) * [Rules Keep Literal Types](#rules-keep-literal-types) * [Type Widening with `default`](#type-widening-with-default) * [Complex Schemas](#complex-schemas) * [Nested Objects](#nested-objects) * [Lists](#lists) * [Discriminated Unions](#discriminated-unions) * [Union Types](#union-types-with-or) * [Custom Rules Type Inference](#custom-rules-type-inference) * [Rules with a Fixed Type](#rules-with-a-fixed-type) * [Rules That Make the Field Required](#rules-that-make-the-field-required) * [Types from Arguments and Earlier Rules](#types-from-arguments-and-earlier-rules) * [Meta Rules](#meta-rules) * [Aliases](#aliases) * [Replacing Built-in Rules](#replacing-built-in-rules) * [Rule Packages](#rule-packages) * [Strict Mode](#strict-mode) * [Migrating from RuleTypeDef](#migrating-from-ruletypedef) * [Limitations](#limitations) * [Available Type Exports](#available-type-exports) *** ## Basic Usage ```typescript import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; const userSchema = { name: ['required', 'string'], email: ['required', 'email'], age: 'positive_integer', } as const; // Infer the type from the schema type User = InferFromSchema; // Result: { name: string; email: string; age?: number } // Pass the inferred type as a generic parameter const validator = new LIVR.Validator(userSchema); // Validate data from external source (API request, form submission, etc.) const input = getUserInput(); const result = validator.validate(input); if (result) { // result is typed as User console.log(result.name); // string console.log(result.age); // number | undefined } ``` > **Important:** Always use `as const` after your schema definition to preserve literal types and enable proper inference. ### Async Validator `AsyncValidator` of `livr/async` takes the same schemas and types, `validate()` returns a promise of the type: ```typescript import LIVR from 'livr/async'; import type { InferFromSchema } from 'livr/types'; const userSchema = { username: ['required', 'string', 'unique_username'], // a custom async rule age: 'positive_integer', } as const; type User = InferFromSchema; const validator = new LIVR.AsyncValidator(userSchema, { autoTrim: true }); try { const user = await validator.validate(input); // User } catch (errors) { // The errors object, or 'FORMAT_ERROR' when the input is not an object } ``` A custom rule has a type in `RuleTypes`, the type of the value after the rule, whether it returns a promise or not: `unique_username: As` (see [Custom Rules Type Inference](#custom-rules-type-inference)). Its function may return a promise of the error code. *** ## How Types Are Inferred Every rule of a field has a type: the type of the value after the rule. The rules run in order, and each one gets the value of the previous one, so the type of the field is the type after its last rule. ### Required vs Optional Fields By default, all fields are **optional**. Use the `required` rule to make a field mandatory. Its place in the list does not matter: ```typescript const schema = { requiredField: ['required', 'string'], // string (required) alsoRequired: ['string', 'required'], // string (required) optionalField: 'string', // string | undefined (optional) } as const; type Data = InferFromSchema; // { requiredField: string; alsoRequired: string; optionalField?: string } ``` The `default` rule also makes fields non-optional since they always have a value, and so does `not_empty_list`, which returns an error for a missing list: ```typescript const schema = { count: [{ default: 0 }, 'integer'], // number (has default) enabled: { default: true }, // boolean (has default, widened) tags: ['not_empty_list', { list_of: 'string' }], // string[] (required) } as const; type Config = InferFromSchema; // { count: number; enabled: boolean; tags: string[] } ``` ### Types of the Built-in Rules | Rule | Inferred Type | |------|---------------| | `string` | `string` | | `integer`, `positive_integer` | `number` | | `decimal`, `positive_decimal` | `number` | | `max_number`, `min_number`, `number_between` | `number` | | `max_length`, `min_length`, `length_equal`, `length_between`, `like` | `string` | | `email`, `url`, `iso_date` | `string` | | `trim`, `remove`, `leave_only` | `string` | | `to_lc`, `to_uc` | `string`, literal types in lower or upper case | | `any_object` | `Record` | | `not_empty`, `equal_to_field` | the type of earlier rules, `string` as the first rule | | `not_empty_list` | the list type of earlier rules or `unknown[]`, the field is required | | `required` | the type of earlier rules, the field is required | | `eq`, `one_of` | the literal types of the allowed values | | `default` | the type of the default value, the field is required | Numeric rules accept strings like `'42'` and return numbers, and string rules return numbers as strings, so `['integer', { max_length: 3 }]` is a `string`. The [built-in rules page](./rules.md) has the whole list. ### Literal Types with `one_of` and `eq` To get literal union types, use `as const` on the schema (or on the values array): ```typescript const schema = { // Without as const: string // With as const: 'admin' | 'user' | 'guest' role: { one_of: ['admin', 'user', 'guest'] }, // The values can also be in an array, the way the LIVR specification writes them plan: { one_of: [['free', 'pro']] }, // Literal type: 'active' status: { eq: 'active' }, } as const; type Data = InferFromSchema; // { role?: 'admin' | 'user' | 'guest'; plan?: 'free' | 'pro'; status?: 'active' } ``` ### Rules Keep Literal Types Rules that only check a value, or convert it to their type, keep a more precise type of earlier rules. A literal union stays a literal union after length checks, `string`, `email` and other string rules, and number literals stay after numeric rules: ```typescript const schema = { role: ['required', { one_of: ['admin', 'user'] }, { max_length: 10 }], level: [{ one_of: [1, 2, 3] }, 'positive_integer'], code: [{ one_of: ['USD', 'EUR'] }, 'to_lc'], name: [{ one_of: [' a', 'b '] }, 'trim'], } as const; type Data = InferFromSchema; // { // role: 'admin' | 'user'; // level?: 1 | 2 | 3; // code?: 'usd' | 'eur'; // name?: string; // trim makes a new string // } ``` ### Type Widening with `default` The `default` rule automatically widens literal types to their primitive base: ```typescript const schema = { // Widens to primitive types count: { default: 0 }, // number (not 0) name: { default: 'anonymous' }, // string (not 'anonymous') active: { default: true }, // boolean (not true) // Use type assertions with unions to preserve specific types status: { default: 'ACTIVE' as 'ACTIVE' | 'PENDING' }, // 'ACTIVE' | 'PENDING' // After other rules, the type of the rules and of the default value role: [{ one_of: ['admin', 'user'] }, { default: 'user' }], // 'admin' | 'user' } as const; type Config = InferFromSchema; // { // count: number; // name: string; // active: boolean; // status: 'ACTIVE' | 'PENDING'; // role: 'admin' | 'user'; // } ``` This is useful because default values typically represent any value of that type, not just the specific default. Use type assertions with union types (`as 'A' | 'B'`) when you need a specific set of allowed values. *** ## Complex Schemas ### Nested Objects ```typescript const schema = { user: { nested_object: { name: ['required', 'string'], email: 'email', address: { nested_object: { city: ['required', 'string'], zip: 'positive_integer', }, }, }, }, } as const; type Data = InferFromSchema; // { // user?: { // name: string; // email?: string; // address?: { // city: string; // zip?: number; // }; // }; // } ``` ### Lists ```typescript const schema = { // List of numbers ids: { list_of: 'positive_integer' }, // List of strings with validation tags: { list_of: ['required', 'string', { max_length: 50 }] }, // List of objects items: { list_of_objects: { id: ['required', 'positive_integer'], name: ['required', 'string'], quantity: 'positive_integer', }, }, } as const; type Data = InferFromSchema; // { // ids?: number[]; // tags?: string[]; // items?: Array<{ // id: number; // name: string; // quantity?: number; // }>; // } ``` ### Discriminated Unions For polymorphic data with a type discriminator: ```typescript const schema = { events: { list_of_different_objects: [ 'type', // Discriminator field { click: { type: { eq: 'click' }, x: ['required', 'integer'], y: ['required', 'integer'], }, scroll: { type: { eq: 'scroll' }, direction: { one_of: ['up', 'down'] }, }, }, ], }, } as const; type Data = InferFromSchema; // { // events?: Array< // | { type: 'click'; x: number; y: number } // | { type: 'scroll'; direction?: 'up' | 'down' } // >; // } ``` `variable_object` works the same way for a single object. The output has only the fields of the schema of the selector value, so list the discriminator field in each schema (`type: 'required'` is enough) to have it in the output and in the type. ### Union Types with `or` ```typescript const schema = { // Can be either a string or a number value: { or: ['string', 'integer'] }, // Required when every set of rules has required id: { or: [['required', 'positive_integer'], ['required', { like: '^[a-z]+$' }]] }, } as const; type Data = InferFromSchema; // { value?: string | number; id: number | string } ``` *** ## Custom Rules Type Inference Types of your rules, aliases and rule packages go to the `RuleTypes` interface of `livr/types/inference`, by declaration merging. Every package adds its own rules, so any number of packages can be used together. camelCase names are found automatically: `my_rule` is also typed as `myRule`, the way the validator registers it. The type of a rule is the type of the value after the rule. The built-in rules are declared the same way, each next to its code in `lib/rules` of the package: `lib/rules/string/max_length.js` and `lib/rules/string/max_length.d.ts`. See the types in the [TypeScript Playground](https://www.typescriptlang.org/play/?#code/PTAEBUAsFNQBQDYEMCeBzATgewK4DsATUAYxmIGsBnUAFxlpQAdpqs8EUAuUASxuoBK+UACMcNGmxJI8eLDVAIsSInkYBbUIyQUkaFop7lYCHgDcMAKB7rGWDAoAyASQBqA0ADNsmgOSmLXwBua1t7BRomWABvUGc8T2gMADEfAGVSaHUkABpQF3cMmGzQAF8vH1B-cwxgSOZKYNC7BwZmUFiAQUo8ztMkSnAosoqsPwDa+pZgHgSk6DxiaCbLEFBOvFAkfspuaAAPHkoaWbRQDBwEA3wCJK3QPGgAdwekdWg8p75ILc2k7AwJCwt1AWE8vH4oKeeEsxDYxy0kDY0CEV2oAF5QABtXzHDCnXx5WKmYzcXwAPQAOpSANQAfixAAYALQATgAutEAIyMnJcgCspQAJL4yuyttQ4XhjiECgIAHSubY8AhISQYeUYaBoI40JJ9HgDaAEAAi0E8SEuNFR0AAFLE8G9oGTGEjHoTzpcWNxXcibT1QP97GSAOoCADyADkAOIAfTgAAkowBRUWlACUIVWYE6nquXnwxBObG4sWOSAclFjXzoZLSAGkAKrMtPSBAIajlyvV7623wN5u+dOgxYfbO-Ih0WCeQvFzZ8c7QGg4DDSkhkKiBnQ-MzbHDQSxyxXK1XqzXa3VJM0Wq3++2WUCgLv8Ht0W2MLWeHj7bh407D6IH0fRdl1XUBbV3BB924fByDkaFh3RAA+DogOA3hwVtI9xB4BB5SOSMsCVKC7Ug-d02HLVQM2G5zVmY0glANYskYSJQDIgwK1gKdQAAKywERQXBXwtQARxwHgtQIXw0OAnhMKmMF2L3WAAEJ0UxXEaHxPA0FFAAffTQFUjj5WfSgQ17D86P2CiQJXTZfDDKM4zgARk2SZwAA0mnQx8qIc0BaK-R4CBCdDSnCsocksDMszWAQL2OJJTnuC58wILADDkBQdTMWAFyQNpnVAW5iGQLVaHoKZqFmUAbSGBp5XHNJtJ4ItQHUYFYGyYxqCK9QjkoZh22NPNYHsNKvVAGskXEe4pnuOFbFw2AgysMqKp64FpuqCw6iiSgZjmLVR1FQDH1mPUMAtJZ6q9RqDAu4DfUebgDQGR6AB5FPBV6US9SgkKix9zNfSB3soL6-104GgNKSwgKupJbtgR7w1Ynh4VQ9C-yLf1uG0-cooRhHYXhBRKEyEpMWe-7uBxMSJKkj1fH+3x2Ri0HyBwUsnxoCsX1musmxbaKgOY7JcLJLJDQQD01kLMZ3jwGhuCK5bGFWwMMABXhNjxhQutuWKJSBaVKbVI4vwMOUiiyJB4rABMsHywE4GwAgcA6yQn2gbj6FmRJdbGqZLEWj2do6zF4mD1Ixnt7IfqiJSqeKJA4fJi3lNMU9JsxR4XiPYiVTVewvsjr2iyQ2004dzMs4RD8o4UTFINLs929PO1YnpqoaQAZgADkZflGS5AAmAeABZ+QANgAdg9SgeZF5tp4ntMG+sTDm6rmgAKAvfvZoMyecYtYYbOIzgvoghTegDsYiAqVKCwK55SUNAIJPMuNX0GgyZdb2EoLaCiIQEZAA): an alias and a rule function with their types, in strict mode. ### Rules with a Fixed Type ```typescript // my-rules.d.ts, or any .ts file of your project import type { As } from 'livr/types/inference'; declare module 'livr/types/inference' { interface RuleTypes { uuid: As; // checks a string to_date: Date; // converts the value to a Date } } ``` ```typescript import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; LIVR.Validator.registerDefaultRules({ uuid, to_date }); const schema = { id: ['required', 'uuid'], createdAt: ['required', 'toDate'], } as const; type Entity = InferFromSchema; // { id: string; createdAt: Date } ``` Use `As` for a rule that checks a value or converts it to `T`: it keeps a more precise `T` of earlier rules. Use the type itself for a rule that makes a new value: | Type of the rule | For | Type of `[{ one_of: ['a', 'b'] }, 'my_rule']` | | --- | --- | --- | | `As` | rules that check a string, or convert a value to a string | `'a' \| 'b'` | | `string` | rules that make a new string, like `trim` | `string` | | `As` | rules that check a value of any type | `'a' \| 'b'` | | `As` | rules that check a list | the list type of earlier rules, or `unknown[]` | ### Rules That Make the Field Required When the field is always in the output after the rule, like with `required` and `default`, the field is not optional. Use `Present` for a rule that always gives `T`, or set `present: true` in an interface that extends `RuleType` (see [the next section](#types-from-arguments-and-earlier-rules)): ```typescript import type { Present, RuleType } from 'livr/types/inference'; // Like `required`, also for a whitespace-only value: the type of earlier rules is kept interface NotBlankType extends RuleType { output: this['input']; present: true; } declare module 'livr/types/inference' { interface RuleTypes { default_now: Present; // sets the current date when the value is empty not_blank: NotBlankType; } } const schema = { updatedAt: 'default_now', title: ['string', 'not_blank'], } as const; type Data = InferFromSchema; // { updatedAt: Date; title: string } ``` `present` is a flag next to `output`, and not a part of it, so that types of nested objects are computed only when they are used. That keeps deeply nested schemas within the limits of TypeScript. ### Types from Arguments and Earlier Rules When the type depends on the rule arguments or on the type before the rule, declare an interface that extends `RuleType`, and compute its `output` from: * `this['args']`: the arguments, as the rule function gets them; * `this['input']`: the type before the rule, `unknown` for the first rule. Set `present: true` when the rule makes the field always present, or compute it from the arguments, like `present: this['args'][0] extends true ? true : false`. | Rule in a schema | The rule function gets | `this['args']` | | --- | --- | --- | | `'my_rule'` | `my_rule(ruleBuilders)` | `[]` | | `{ my_rule: 'a' }` | `my_rule('a', ruleBuilders)` | `['a']` | | `{ my_rule: [1, 10] }` | `my_rule(1, 10, ruleBuilders)` | `[1, 10]` | | `{ my_rule: [['a', 'b']] }` | `my_rule(['a', 'b'], ruleBuilders)` | `[['a', 'b']]` | ```typescript import type { RuleType } from 'livr/types/inference'; // { currency: ['USD', 'EUR'] } -> 'USD' | 'EUR' interface CurrencyType extends RuleType { output: this['args'][number]; } // { instance_of: Date } -> Date interface InstanceOfType extends RuleType { output: this['args'][0] extends abstract new (...args: any) => infer T ? T : unknown; } // 'to_list': a list of the value, or the list itself interface ToListType extends RuleType { output: this['input'] extends readonly unknown[] ? this['input'] : Array; } declare module 'livr/types/inference' { interface RuleTypes { currency: CurrencyType; instance_of: InstanceOfType; to_list: ToListType; } } const schema = { price: ['required', { currency: ['USD', 'EUR'] }], createdAt: { instance_of: Date }, ids: ['positive_integer', 'to_list'], } as const; type Data = InferFromSchema; // { price: 'USD' | 'EUR'; createdAt?: Date; ids?: number[] } ``` Two things to know when you write such types: * Read the arguments by index, `this['args'][0]` or `this['args'][number]`. Inside the interface they are an intersection with `readonly unknown[]`, so `this['args'] extends [infer First, ...infer Rest]` gives wrong types. * `this` is not allowed in an object type literal: instead of `output: { currency: this['args'][0] }`, declare `interface Money { amount: number; currency: C }` and set `output: Money`. ### Meta Rules Meta rules validate their value with other rules, and their types are built from the types of those rules: * `InferFromSchema`: the type of an object validated by a schema, like `nested_object`; * `InferRuleType`: the type of a value after a rule or a list of rules, like the items of `list_of`. `InferRuleType` applies the rules to the value of your rule, like a set of rules of `or`; * `MakesPresent`: `true` when the rules make the field present, for `present` of such a rule. ```typescript import type { RuleType, InferRuleType, LIVRRuleDefinition } from 'livr/types/inference'; // { dict_of: 'positive_integer' } or { dict_of: ['required', 'string'] } // -> Record, Record interface DictOfType extends RuleType { params: readonly LIVRRuleDefinition[]; output: Record>; } declare module 'livr/types/inference' { interface RuleTypes { dict_of: DictOfType; } } ``` `params` are the types of the arguments, a tuple like `this['args']`: [strict mode](#strict-mode) checks the arguments with it, and the rules in them. A single argument can also be written without the array, like `{ dict_of: 'string' }`. `readonly []` is a rule without arguments, `...ignored: unknown[]` at the end allows extra arguments that the rule ignores. Without `params`, the arguments are not checked. `As` sets them for a rule with a fixed type. The built-in rules are written this way, see `lib/rules/meta/*.d.ts` and the other `lib/rules/**/*.d.ts` in the package. ### Aliases The type of an alias is the type of its rules written in place, `AliasType`. Keep the rules in a constant with `as const`, and pass it both to the validator and to the type: ```typescript import type { AliasType } from 'livr/types/inference'; const phoneRules = ['string', { like: '^\\+?[0-9]{10,15}$' }] as const; const requiredPhoneRules = ['required', 'phone'] as const; LIVR.Validator.registerAliasedDefaultRule({ name: 'phone', rules: phoneRules, error: 'WRONG_PHONE' }); LIVR.Validator.registerAliasedDefaultRule({ name: 'required_phone', rules: requiredPhoneRules }); declare module 'livr/types/inference' { interface RuleTypes { phone: AliasType; required_phone: AliasType; } } const schema = { phone: 'phone', backupPhone: 'requiredPhone', } as const; type Contact = InferFromSchema; // { phone?: string; backupPhone: string } ``` An alias gets the type of earlier rules, like its rules would, and `required` in an alias makes the field required. Aliases of one validator (`validator.registerAliasedRule()`) and its own rules (`validator.registerRules()`) are typed the same way: rule types are global. The type is not added by `registerAliasedDefaultRule()`: an alias without a type in `RuleTypes` gives `unknown`. [Strict mode](#strict-mode) makes it a compile error. The order of the call and the declaration does not matter, TypeScript reads all declarations first. ### Replacing Built-in Rules `registerDefaultRules()` and `registerAliasedDefaultRule()` can replace a built-in rule. A type in `RuleTypes` replaces the type of the built-in rule with the same name in the same way: ```typescript // Prices stay strings with two decimals const priceRules = ['string', { like: '^\\d+\\.\\d{2}$' }] as const; LIVR.Validator.registerAliasedDefaultRule({ name: 'decimal', rules: priceRules }); declare module 'livr/types/inference' { interface RuleTypes { decimal: AliasType; // string instead of number } } ``` The types of the built-in rules are in `BuiltInRuleTypes`. Reuse them for your rules, like `my_enum: BuiltInRuleTypes['one_of']`. ### Rule Packages A package with LIVR rules declares their types the same way, in a `.d.ts` file that its types entry loads, so the types come with `import 'my-livr-rules'`. Put the type of each rule next to its code (`rules/uuid.js` and `rules/uuid.d.ts`), or all of them in one file: ```typescript // my-livr-rules/types/index.d.ts import type { As, RuleType } from 'livr/types/inference'; // { is: 'active' } -> 'active', the field is required interface IsType extends RuleType { output: this['args'][0]; present: true; } declare module 'livr/types/inference' { interface RuleTypes { uuid: As; boolean: boolean; list_length: As; is: IsType; } } type RuleFactory = ( ...args: Array ) => (value: any, params: any, outputArr: Array) => string | undefined; declare const rules: Record; // At the top level: inside `declare module 'my-livr-rules'` in this file it would be // a module augmentation, where exports are not allowed (TS2666 without skipLibCheck) export = rules; ``` * List `livr` in `peerDependencies`, so that the package and the application use the same `livr/types/inference`. * Rule names are global. Two packages may declare the same rule with the same type. With different types, TypeScript reports TS2717 (`Subsequent property declarations must have the same type`); with `skipLibCheck` the error in a package is hidden and the first declaration wins. * Check the package in a project that installs it (`npm pack`), with and without `skipLibCheck`. ```typescript import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; import myRules from 'my-livr-rules'; // the rules and their types LIVR.Validator.registerDefaultRules(myRules); const schema = { id: ['required', 'uuid'], status: { is: 'active' }, } as const; type Item = InferFromSchema; // { id: string; status: 'active' } ``` ## Strict Mode A rule without a type gives `unknown`, with no error: a misspelled rule, or a custom rule or an alias whose type was forgotten. Strict mode makes it a compile error. It is off by default, so existing projects keep compiling. Turn it on in new projects, and in existing ones once every rule has a type. One declaration turns it on for the whole application: ```typescript // livr-options.ts, or any .ts file of your project. The import makes the file a module and // loads the types of livr: without an import or an export, the file would declare a new module import type {} from 'livr/types/inference'; declare module 'livr/types/inference' { interface TypeOptions { strictRules: true; } } ``` Then every rule must have a type: a built-in rule, a type in `RuleTypes`, or a rule of a package with types. TypeScript checks: * schemas passed to `new LIVR.Validator()`, also the rules inside meta rules, like `nested_object`, `list_of` and `or`; * schemas with `satisfies LIVRSchema`; * the names and the rules of aliases, `registerAliasedDefaultRule()` and `registerAliasedRule()`; * the names of rules in `registerDefaultRules()` and `registerRules()`, when the object is written in place. Rules of a package typed as `Record` are not checked there, their types are. ```typescript const schema = { name: ['required', 'strng'], } as const; new LIVR.Validator(schema); // Type '"strng"' is not assignable to type 'StrictRule'. Did you mean '"string"'? LIVR.Validator.registerAliasedDefaultRule({ name: 'phone', rules: phoneRules }); // Type '"phone"' is not assignable to type 'RuleName', until RuleTypes has `phone` ``` With `as const satisfies LIVRSchema` the error is shown at the rule, not at the validator: ```typescript import type { LIVRSchema } from 'livr/types'; const schema = { name: ['required', 'strng'], // Did you mean '"string"'? } as const satisfies LIVRSchema; ``` Strict mode does not change the inferred types. It also checks the arguments of the rules by `params` of their types (see [Meta Rules](#meta-rules)), like the validator does when it builds the rules: * the rules in the arguments of meta rules: `{ nested_object: 'string' }` and `{ when: ['type', 'required', []] }` are errors (an empty list of rules in `when`, `values` and `keys` is a schema error, use `'any'`); * a LIVR 2.5 rule without arguments takes none: `{ boolean: 1 }`, `{ jwt: 'HS256' }` are errors; * a number may be a numeric string and a string may be a number, as the validator reads them: `{ max_decimals: '2' }`, `{ min_length: '5' }`, `{ split: 1 }`, `{ mac: '64' }`; values are checked, `{ uuid: 4 }` and `{ mac: 32 }` are errors; * `iso_date`, `iso_datetime` and `iso_time` take nothing or an options object first: `{ iso_date: 'today' }` is an error; * the arguments after the ones a rule takes are allowed, the validator ignores them: `{ split: [',', ';'] }` splits at `,`; * the LIVR 2.0 rules without arguments (`required`, `email`, `trim`, `integer`...) take any arguments, the validator ignores them as in LIVR 2.0: `{ required: true }` compiles. A custom rule gets the same checks with `params`, or with the second argument of `As`: `As`. Without them its arguments are not checked. * **A schema without `as const`** has `string` instead of the rule names, every rule is reported. Inference needs `as const` anyway. * **A schema from JSON or built at runtime** is not checked: `new LIVR.Validator(schema as LIVRSchema)`. * **Deep schemas.** TypeScript compares types to about 100 levels, so strict mode checks up to 16 levels of nested meta rules, like `nested_object` in `nested_object` (8 levels of `or` with `nested_object` in it). A deeper schema gets `Excessive stack depth comparing types`: pass it `as LIVRSchema`. Inference works deeper. * `RuleName` is the union of the names of the rules with a type. `strictRules: false` turns strict mode off explicitly, the same as no option. *** ## Migrating from RuleTypeDef Earlier versions declared rule types with `RuleTypeDef` in `RuleTypeRegistry` and with templates in `ParameterizedRuleRegistry`. These declarations still work, so packages and projects can move to `RuleTypes` when they like. They are deprecated (editors show them struck through) and will be removed in the next major version. | Before | After | | --- | --- | | `interface RuleTypeRegistry { ... }` | `interface RuleTypes { ... }` | | `my_rule: RuleTypeDef` | `my_rule: As` when the rule checks or converts to a string, `my_rule: string` when it makes a new string | | `myRule: RuleTypeRegistry['my_rule']` | remove: camelCase names are found automatically | | `RuleTypeDef`, `DefaultRule`, `RuleTypeDef` | `Present` | | `RequiredRule` | an interface with `output: this['input']` and `present: true` | | `RuleTypeDef` for a rule that only checks the value | `As`, or `As` for a list. The field keeps its type now: before, it became `unknown` | | `ParameterizedRuleDef<'literal'>` | an interface with `output: this['args'][0]` | | `ParameterizedRuleDef<'array_element'>` | `BuiltInRuleTypes['one_of']` | | `ParameterizedRuleDef<'infer_schema'>` | an interface with `output: InferFromSchema` | | `ParameterizedRuleDef<'infer_schema_array'>` | an interface with `output: Array>` | | `ParameterizedRuleDef<'infer_rule_array'>` | `BuiltInRuleTypes['list_of']` | | A template in `TemplateOutputRegistry` | an interface with the template as `output`, `this['args'][0]` instead of `Args` | | `RuleTypeRegistry['email']` (types of built-in rules) | `BuiltInRuleTypes['email']` | Templates got the arguments as they are written in the schema, `this['args']` has them as the rule function gets them. They differ only for an array: `{ my_rule: ['a'] }` gave `['a']` to a template, and `this['args'][0]` is `'a'`, like the first argument of the rule function. A package with both registries, before and after: ```typescript // Before import type { RuleTypeDef, ParameterizedRuleDef } from 'livr/types/inference'; declare module 'livr/types/inference' { interface TemplateOutputRegistry { instance_of: Args extends abstract new (...args: any) => any ? InstanceType : unknown; } interface RuleTypeRegistry { uuid: RuleTypeDef; credit_card: RuleTypeDef; creditCard: RuleTypeRegistry['credit_card']; boolean: RuleTypeDef; list_length: RuleTypeDef; listLength: RuleTypeRegistry['list_length']; } interface ParameterizedRuleRegistry { is: ParameterizedRuleDef<'literal', true, false>; instance_of: ParameterizedRuleDef<'instance_of', false, false>; instanceOf: ParameterizedRuleDef<'instance_of', false, false>; } } ``` ```typescript // After import type { As, RuleType } from 'livr/types/inference'; interface IsType extends RuleType { output: this['args'][0]; present: true; } interface InstanceOfType extends RuleType { output: this['args'][0] extends abstract new (...args: any) => infer T ? T : unknown; } declare module 'livr/types/inference' { interface RuleTypes { uuid: As; credit_card: As; boolean: boolean; list_length: As; is: IsType; instance_of: InstanceOfType; } } ``` Types of the built-in rules changed in the same version. They follow the runtime more closely, so some fields get more precise types, see the [changelog](https://js.livr-spec.org/changelog). *** ## Limitations * **`as const` is needed.** Without it, a list of rules is a `string[]`, and its type is `unknown`. * **Rule names are checked only in [strict mode](#strict-mode).** Otherwise a misspelled rule, or a rule without a declared type, gives `unknown`, as the type of a value after it is not known. * **Empty values.** Rules skip empty values (`null` and `''`) of optional fields, and the value stays in the output as it is: `{ age: 'integer' }` with the input `{ age: '' }` gives `{ age: '' }`, but the type is `{ age?: number }`. The same holds for items of `list_of` without `required`. Use `required` or `default` where the type must hold for such input. * **Schemas built at runtime** have no compile-time type. Declare the type of the output where you load the schema, and pass it to `new LIVR.Validator(schema)`. * **TypeScript 5.0 or newer.** *** ## Available Type Exports ```typescript import type { // Main inference type InferFromSchema, // Infer type from a single rule (useful for list elements) InferRuleType, // true when rules make the field present, for meta rules MakesPresent, // Schema and rule definition types LIVRSchema, LIVRRuleDefinition, LIVRPrimitive, // Types of rules: add yours to RuleTypes in 'livr/types/inference' RuleTypes, BuiltInRuleTypes, RuleName, RuleType, As, Present, AliasType, // Utility types Simplify, } from 'livr/types'; ``` `TypeOptions` of `livr/types/inference` sets [strict mode](#strict-mode): `strictRules: true` turns it on, `strictRules: false` keeps it off. `livr/types/inference` exports the same types, and it is the module to add rule types to. The deprecated `RuleTypeDef`, `RuleTypeRegistry`, `ParameterizedRuleDef`, `ParameterizedRuleRegistry`, `SimpleRule`, `RequiredRule`, `ParameterizedRule` and `DefaultRule` are still exported, see [Migrating from RuleTypeDef](#migrating-from-ruletypedef). --- --- url: https://js.livr-spec.org/api.md description: >- Validator and AsyncValidator: the constructor and its options, validate(), getErrors(), registering rules and aliases, and the exports of livr and livr/async. --- # API `Validator` (from `livr`) and `AsyncValidator` (from `livr/async`) have the same API, except that the async `validate()` returns a promise and there is no async `getErrors()`. ## Static methods ### `new LIVR.Validator(schema, options?)` Creates a validator. ```js const validator = new LIVR.Validator(schema, { autoTrim: true }); ``` | Option | Default | Description | | ---------- | ------- | ---------------------------------------- | | `autoTrim` | `false` | Trim all string values before validation | The bounds `today`, `yesterday` and `tomorrow` of `iso_date` and `iso_datetime` are days in UTC of the system clock, never in the time zone of the server. A rule reads `Date.now()` whenever it checks a value with such a bound, so a long-lived validator follows the clock, and tests fix the time with fake timers (`mock.timers` of `node:test`, `vi.useFakeTimers()`, `jest.useFakeTimers()`) or a stub of `Date.now()`: ```js import { test } from 'node:test'; test('a birth date is not in the future', t => { t.mock.timers.enable({ apis: ['Date'], now: new Date('2026-10-29T12:00:00Z') }); const validator = new LIVR.Validator({ birth_date: ['required', { iso_date: { max: 'today' } }] }); validator.validate({ birth_date: '2026-10-30' }); // false, { birth_date: 'DATE_TOO_LATE' } }); ``` ### `Validator.defaultAutoTrim(boolean)` Turns auto-trim on or off for all validators created afterwards. ```js LIVR.Validator.defaultAutoTrim(true); ``` ### `Validator.registerDefaultRules(rules)` Registers rules for all validators. ```js LIVR.Validator.registerDefaultRules({ my_rule(arg) { return value => { /* ... */ }; }, }); ``` ### `Validator.registerAliasedDefaultRule(alias)` Registers a rule alias for all validators. ```js LIVR.Validator.registerAliasedDefaultRule({ name: 'adult_age', rules: ['positive_integer', { min_number: 18 }], error: 'MUST_BE_ADULT', // optional }); ``` ### `Validator.getDefaultRules()` Returns a new object with all default rules: the built-in rules of the validator class and the rules registered with `registerDefaultRules()` and `registerAliasedDefaultRule()`. ## Instance methods ### `validator.validate(data)` Validates the data against the schema. Sync validator: ```js const result = validator.validate(data); if (result) { // result contains the validated data } else { const errors = validator.getErrors(); } ``` Async validator: ```js try { const result = await validator.validate(data); } catch (errors) { // the errors object } ``` ### `validator.getErrors()` Returns the errors of the last validation (sync validator only). ```js { email: 'WRONG_EMAIL', password: 'TOO_SHORT', address: { zip: 'NOT_POSITIVE_INTEGER' }, } ``` ### `validator.prepare()` Compiles the rules. `validate()` calls it on the first validation; call it yourself to warm up. ```js const validator = new LIVR.Validator(schema).prepare(); ``` ### `validator.getFieldRule(fieldName)` Returns all rules of a schema field as one rule function `(value, allValues, outputArr) => errorCode`, prepared once. It runs the rules like `validate()` does for that field: in order, each rule gets the output of the previous one. On success it returns `undefined`. Like any rule, it pushes the output value to `outputArr` only when the rules change the value, so a missing value stays missing. With `AsyncValidator` it returns a promise only when some rule returns a promise. With `autoTrim` the value is trimmed before the rules run; `allValues` are passed to the rules as given. Use it in custom meta rules that validate many values with the same rules: it skips the result and errors objects that `validate()` builds for every call. Built-in `list_of` and `or` work this way. ```js function list_of_tags(itemRules, ruleBuilders) { const validateItem = new LIVR.Validator({ item: itemRules }) .registerRules(ruleBuilders) .getFieldRule('item'); return (values, params, outputArr) => { if (!Array.isArray(values)) return 'FORMAT_ERROR'; const output = []; for (const value of values) { const itemOutput = []; const errorCode = validateItem(value, params, itemOutput); if (errorCode) return errorCode; // The value as is, unless the rules changed it output.push(itemOutput.length ? itemOutput[0] : value); } outputArr.push(output); }; } ``` ### `validator.registerRules(rules)` Registers rules for this validator only. ```js validator.registerRules({ custom_rule() { return value => { /* ... */ }; }, }); ``` ### `validator.registerAliasedRule(alias)` Registers a rule alias for this validator only. ```js validator.registerAliasedRule({ name: 'strong_password', rules: { min_length: 8 }, error: 'WEAK_PASSWORD', }); ``` ### `validator.getRules()` Returns all rules registered for this validator. ## Exports | Import | What | | ------------------------------------------ | ---------------------------------------------------------------------- | | `import LIVR from 'livr'` | `{ Validator, rules, util }` with all built-in rules registered | | `import LIVR from 'livr/async'` | `{ AsyncValidator, rules, util }` with the async meta rules | | `import Validator from 'livr/lib/Validator'` | the bare validator, no rules registered, for [smaller bundles](./guide/usage.md#smaller-bundles) | | `import type { ... } from 'livr/types'` | `InferFromSchema` and the other [types](/typescript.md#available-type-exports) | --- --- url: https://js.livr-spec.org/changelog.md description: Changes in every version of livr. --- ## CHANGELOG v2.13.3 * Security fix in `url` * Fix: `url` rejects a dot after the host (`https://example.com.`), host labels longer than 63 characters and ports above 65535, and accepts one-digit ports (`https://example.com:8`). Unicode hosts, `localhost` and IPv4 addresses pass as before v2.13.2 * Docs: the rules tables of the README and the site follow the groups of the specification. The LIVR 2.5 rules are in their groups, marked 2.5, and in the new list, format and checksum groups, instead of a separate list. The rules link to their pages on livr-spec.org, the groups to the pages of the groups v2.13.1 * Docs: LIVR 2.5 is released, not a draft. The README and the site say that `livr` implements LIVR 2.0 and LIVR 2.5, link the LIVR 2.5 specification, and the rules of LIVR 2.5 link to their pages on livr-spec.org v2.13.0 * The rules of the LIVR 2.5 draft are built in: `boolean`, `not_one_of`, `digits`, `starts_with`, `ends_with`, `contains`, `max_decimals`, `iso_datetime`, `iso_time`, `required_if`, `when`, `values`, `keys`, `any`, `list_length`, `unique`, `split`, `uuid`, `ip`, `cidr`, `hostname`, `base64`, `hex`, `digest`, `mongo_id`, `hex_color`, `mac`, `e164`, `semver`, `jwt`, `luhn`, `credit_card` and `iban`, in `Validator` and `AsyncValidator`, with their TypeScript types. LIVR 2.5 is a draft of the next minor version of the specification: details of these rules may still change in a minor release. See [Built-in rules](https://js.livr-spec.org/rules#livr-25-draft-rules) * `iso_date` takes bounds: `{ iso_date: { min: '2000-01-01', max: 'today' } }` gives `DATE_TOO_EARLY` and `DATE_TOO_LATE`. `today`, `yesterday` and `tomorrow` are days in UTC of the system clock, read whenever a value is checked, so tests fix the time with fake timers (see the [API](https://js.livr-spec.org/api)). `iso_datetime` takes the same bounds * The new rules are loaded on first use, so a schema without them does not pay for them at startup. Bundles grow: about 13 KB min+gzip with all 68 rules (5 KB before). The 2.0 rules imported one by one stay about 5 KB * A rule registered under one of the new names replaces the built-in rule, so schemas with `boolean`, `uuid` or `required_if` of livr-extra-rules keep working while the package is registered * The new rules check their arguments when a validator is built: a wrong argument, an unknown option and any argument of a rule without arguments are schema errors, also `{ rule: null }` (`{ boolean: null }`, `{ uuid: null }`). The LIVR 2.0 rules and custom rules get `null` as their argument, as before * Fix: `equal_to_field` and custom rules that read other fields saw a made-up object `{ value: x }` inside an alias and `{ field: x }` inside an `or` branch, so a comparison with a sibling field never worked there. Now they see the siblings of the field. In an item of `list_of` they see no fields (before `{ field: item }`), so `{ list_of: { equal_to_field: 'field' } }` no longer compares an item with itself * Fix: `iso_date` ignored its arguments, so `{ iso_date: 'today' }` checked nothing of what it says. An argument that is not an options object is a schema error now. `'iso_date'`, `{ iso_date: [] }` and `{ iso_date: null }` are the plain rule, as before * Fix: the meta rules (`nested_object`, `variable_object`, `list_of` with a list of rules, `list_of_objects`, `list_of_different_objects`) ignore extra arguments, as the other rules do. Before, an extra argument took the place of the rules registered for the validator. `like` with flags and an extra argument ignored the flags * Types: strict mode checks the arguments of every built-in rule, like the validator does: `{ boolean: 1 }`, `{ uuid: 4 }` and `{ iso_date: 'today' }` are errors, a number may be a numeric string. The LIVR 2.0 rules without arguments take any arguments, as the validator ignores them. `As` sets the types of the arguments of a custom rule. See the [TypeScript guide](https://js.livr-spec.org/typescript#strict-mode) * Tests: the LIVR 2.0 test suite is updated (`iso_date` in the proleptic Gregorian calendar, `like` anchors, `decimal`, `list_of` and modifier cases), and the LIVR 2.5 draft suite runs too, with its schema errors v2.12.1 * Docs: the strict mode snippet imports `livr/types/inference` instead of `export {}`, so it turns strict mode on in any file: the import also loads the types of livr. Before, the file worked only when another file of the program imported livr * README: the links to the documentation are a list v2.12.0 * The npm package contains only `lib`, `async.js`, `async.d.ts`, `types`, the guides, the API and the TypeScript guide (`docs/`), README, LICENSE and CHANGELOG (98 files instead of 341): tests, editor and CI configs are no longer published * Code examples in README and docs use 2-space indentation, like the code * Docs: new documentation site [js.livr-spec.org](https://js.livr-spec.org), built from `docs/` and deployed to GitHub Pages. The README is short now, with links to the site * Docs: `docs/DESIGN.md` starts with the decisions behind the main benefits (schemas as data, no code generation, performance, zero dependencies, the specification). Sizes are re-measured: about 2 KB min+gzip for the core and 5 KB with all rules (the README said < 1 KB and ~3 KB). The code-injection example shows the actual result, and the list of implementations matches livr-spec.org * Types: rule types are in `RuleTypes` (your rules, aliases and rule packages) and `BuiltInRuleTypes` (built-in rules), declared with `As`, `Present`, `AliasType` or an interface that extends `RuleType`, reads the rule arguments and the type of earlier rules, and sets `present: true` for a rule that makes the field present. Every rule has its whole type in the `.d.ts` file next to it, meta rules included: `types/inference.d.ts` has no list of built-in rules anymore. camelCase names are typed automatically. A type in `RuleTypes` replaces the type of a built-in rule, like `registerDefaultRules()` and `registerAliasedDefaultRule()` replace the rule. For meta rules, `InferRuleType` gets the type of a value before the rules, and `MakesPresent` is `true` when the rules make the field present. See the [TypeScript guide](https://js.livr-spec.org/typescript#custom-rules-type-inference) * Types: `RuleTypeDef`, `RuleTypeRegistry`, `ParameterizedRuleDef`, `ParameterizedRuleRegistry`, `TemplateOutputRegistry`, `SimpleRule`, `RequiredRule`, `ParameterizedRule` and `DefaultRule` are deprecated and will be removed in the next major version. They still work, and come before the built-in rule types: livr-extra-rules 1.6.1 is typed as before. `RuleTypeRegistry` does not list the built-in rules anymore, `RuleTypeRegistry['email']` is `BuiltInRuleTypes['email']` now. The [migration guide](https://js.livr-spec.org/typescript#migrating-from-ruletypedef) maps every old declaration to the new one * Types of the built-in rules follow the runtime more closely, so some fields get more precise types: * `required` keeps the type of earlier rules: `['string', 'required']` was `unknown`, now it is `string`. The same for `RequiredRule` of older packages * Rules that check or convert a value keep a more precise type of earlier rules: `[{ one_of: ['a', 'b'] }, { max_length: 1 }]` was `string`, now it is `'a' | 'b'`. The same for `string`, `like`, the length, numeric and special rules, `not_empty` and `any_object`. `not_empty` and `equal_to_field` keep the type, so `['integer', 'not_empty']` is a `number` * `to_lc` and `to_uc` change the case of literal types: `[{ one_of: ['A', 'B'] }, 'to_lc']` is `'a' | 'b'` * `not_empty_list` makes the field required, as a missing list is an error, and keeps the list type of earlier rules * `one_of` and `list_of` with the values or rules in an array, as the specification writes them: `{ one_of: [['a', 'b']] }` was the array type, now it is `'a' | 'b'`. `{ list_of: [['required', 'integer']] }` was `unknown[]`, now it is `number[]` * `default` after other rules keeps their type: `[{ one_of: ['a', 'b'] }, { default: 'a' }]` was `string`, now it is `'a' | 'b'`. An array default (`{ default: [[1, 2]] }`) has the type of the array, the value the rule gets * `or` makes the field required when every set of rules does (`{ or: [['required', 'string'], ['required', 'integer']] }`), and its sets of rules get the type of earlier rules * `variable_object` and `list_of_different_objects`: the selector field is in the type only when the schema of the selector value has it, as the output has only the fields of that schema * Types: `registerAliasedRule()` and `registerAliasedDefaultRule()` accept a rule name as `rules` (`rules: 'required'`), like at runtime * Types of `livr/async`: `AsyncValidator` with the same schemas and type inference as `Validator`, `validate()` returns a promise of the type, rules may return promises. `livr/async` had no types before, `import LIVR from 'livr/async'` failed to compile with `strict` * Types: the `options` argument of the constructor (`{ autoTrim: true }` or a boolean), `validator.getRules()` and `LIVR.util` were missing. `validator.getAliasedRule()` and `LIVR.ValidatorError` are deprecated and will be removed in the next major version: they do not exist at runtime * Types: strict mode, `strictRules: true` in `TypeOptions` of `livr/types/inference`: a rule without a type is a compile error, instead of the `unknown` type. It catches misspelled rules and forgotten types of custom rules and aliases, in schemas of validators (also inside meta rules), with `satisfies LIVRSchema`, in aliases and in the names of registered rules. Meta rules declare their arguments with `params`, custom meta rules can too. Off by default, so existing projects keep compiling, and recommended for new projects: README, the guides, `llms.txt` and `context7.json` say so. `strictRules: false` turns it off explicitly. See the [TypeScript guide](https://js.livr-spec.org/typescript#strict-mode). `RuleName` is the union of the names of the rules with a type * Type checks of the package check its declaration files too (`skipLibCheck` is off), and cover every built-in rule, custom rules, aliases, the deprecated registries, an application with several rule packages, strict mode on and turned off with `strictRules: false`, and a program with only `livr/types/inference` (`npm run test:types`) * Docs: the TypeScript guide covers types of rules with arguments, meta rules, aliases, replaced built-in rules, rule packages, strict mode, the migration from `RuleTypeDef` and the limitations. A new TypeScript Playground example shows custom rules with their types in strict mode. A TypeScript example on the design decisions page used `'boolean'`, a rule of livr-extra-rules, as a built-in rule * Docs for AI assistants: the site publishes [llms.txt](https://js.livr-spec.org/llms.txt), [llms-full.txt](https://js.livr-spec.org/llms-full.txt) and a Markdown copy of every page, and `context7.json` sets up [Context7](https://context7.com). The guides, the API and the TypeScript guide are in the package (`node_modules/livr/docs`), and pages link each other with relative links, which work there and on GitHub. The README shows custom rules with their types, and the declarations of `registerRules()`, `registerAliasedRule()` and `RuleTypes` say how to declare the type of a custom rule, with links to the guide v2.11.0 * Fix: `livr/async`, `livr/lib/*` (tree-shaking) and `livr/types` were not importable since 2.9.2 (`ERR_PACKAGE_PATH_NOT_EXPORTED`), as the `exports` field listed only `.` and `./types/inference` * Fix: `validate()` threw a `TypeError` when the input had its own `hasOwnProperty` key * Fix: when both `livr` and `livr/async` were loaded, the one loaded last replaced meta rules (`nested_object`, `list_of`, etc.) of the other. Built-in rules are now stored per validator family. Rules registered through `registerDefaultRules()` and `registerAliasedDefaultRule()` are still shared by both classes, so custom rules and overrides of built-in rules work in both (async rules only in `AsyncValidator`, as before). `getDefaultRules()` returns a new object with the built-in and registered rules * Fix: `list_of_different_objects` passed a list when all its invalid items were not objects or had a missing or unknown selector value, and dropped such items from the output. A `null` item threw a `TypeError` * Fix: `variable_object` and `list_of_different_objects` threw a `TypeError` when the selector value was the name of an `Object.prototype` property (`"constructor"`, `"toString"`, `"__proto__"`, ...), and accepted arrays as selector values. Both return `FORMAT_ERROR` now * Fix: missing fields named like `Object.prototype` methods (`constructor`, `toString`, `hasOwnProperty`, ...) got the inherited method as a value: `required` passed and `default` was not applied * Fix: objects with own `constructor` key failed with `FORMAT_ERROR` (on any level) and were not auto trimmed * Fix: rule names like `toString` or `constructor` were found on `Object.prototype`: instead of `Rule [toString] not registered`, validation failed later with a `TypeError` * Fix: `registerAliasedDefaultRule()` did not register the camelCase name of an alias (`myAlias` for `my_alias`), and overriding a default rule did not update its camelCase name * Fix: `AsyncValidator`: an exception thrown by a rule inside `or` or an alias without its own error code was lost, and the value passed validation. Inside `list_of` the error of such item was `undefined`. The exception is the error now, as in `nested_object` * Objects without prototype (`Object.create(null)`, `querystring.parse()` output) are validated as objects, `util.isObject()` returns `true` for them * `util.isThenable()` and `util.getFieldValue()` (value of a data field; inherited methods like `constructor` are not taken as values) * `validator.getFieldRule(fieldName)` (sync and async): all rules of a schema field as one rule function, for custom meta rules that validate many values with the same rules. Like any rule, it pushes the output value only when the rules change it. Built-in `list_of`, `or` and aliases use it * Fix: an alias or `or` on a missing field added the field to the output with the value `undefined` (the key was there, unlike with other rules). A missing field stays missing now * Test suite: sync with the LIVR spec repository, which got cases for the fixes above * Performance (no API changes; compared with 2.10.2 on Node.js 26): * Sync validator: 1.1-1.5x faster for flat and nested forms, `list_of` 2.1x, `or` and aliases 1.7x, `iso_date` 2.5x, `email` 2.4-3x, `one_of` with long lists up to 4.7x * Validator construction (new validator per validation): 1.6x faster * Async validator: 2.2-2.5x faster. Rules that return plain values no longer cost an `await`. With synchronous rules the key order of results and errors is the same as before; with rules returning promises (async meta rules, aliases, custom async rules) it may differ, as it depended on microtask timing * Async `or`, `list_of` and aliases return their result without a promise while their rules are synchronous, so such fields need no `await` either * Benchmarks moved to the separate `validators-benchmarks` repository: `benchmarks/bench.js`, the `bench` script and the `benchmark` dev dependency are removed * Tests: migrated from AVA and nyc to the built-in `node:test` and `node:assert/strict` with built-in coverage, both dev dependencies are removed. Running the tests needs Node.js 22.8+ * CI: GitHub Actions (Node.js 22, 24 and 26) instead of Travis CI * Removed the `dist/` browser bundles (last built for 2.7.1, so later releases shipped outdated code), the webpack build and the `livr/dist/*` export. Use a bundler or an ESM CDN (see README). Dev dependencies are down to TypeScript * Type checks of the package run with TypeScript 7 (the type declarations still work with TypeScript 5) * Docs: re-measure the comparison with Zod, Valibot, Joi, fastest-validator and Ajv in `docs/DESIGN.md` and README v2.10.2 * Security hardening: Skip `__proto__` key in `_autoTrim` and `prepare()` to prevent local prototype manipulation * This is a low-severity issue: it could only affect validation logic when `autoTrim: true` and attacker-controlled JSON input contains `__proto__` property. Global `Object.prototype` was never at risk. * Add security test suite for prototype pollution vectors v2.9.4 * Type inference: `default` rule now widens literal types to primitives (e.g., `{default: 10}` infers as `number` instead of `10`) * Use type assertions with unions to preserve specific types (e.g., `{default: 'ACTIVE' as 'ACTIVE' | 'PENDING'}`) * Add TypeScript type inference tests (`t/types-test.ts`) * Add `tsconfig.json` for type checking * `npm test` now includes TypeScript type checks v2.9.3 * Add support for custom templates for complex cases of type inference v2.9 * Add types inference engine v2.8.1 * Add basic types for typescript v2.7.1 * 300-500% faster validator construction. Useful when you construct new Validator for each validation. * Remove "camelizeRules" option. Now it is always enabled but does not overried existing rules if there is a conflict. v2.7 * 25% faster validation comparing to v2.5 * Automatically camelize default rules (both names can be used in schemas) * Validator constructor now accepts "options" object. Required for future options extensability --- --- url: https://js.livr-spec.org/why/schemas-are-data.md description: >- A LIVR schema is a plain JSON structure. Store it in a database, share it across languages, and load it safely from untrusted sources. --- # Rules are just data A LIVR schema is a plain data structure: objects, arrays, strings and numbers. It contains no functions and no classes, and the validator never turns it into code. This is the decision everything else in LIVR follows from. > **Portability and safety** are the priority → schemas are **pure JSON data** → no fluent API and no inline functions in a schema ## Data, not code The same rules as LIVR data and as Zod code: ::: code-group ```js [LIVR] const schema = { username: ['required', { min_length: 3 }, { max_length: 50 }], email: ['required', 'email'], role: { one_of: ['admin', 'user', 'guest'] }, }; // It is data: it survives JSON.stringify() and JSON.parse() unchanged await db.saveSchema('user-registration', JSON.stringify(schema)); const loaded = JSON.parse(await db.getSchema('user-registration')); const validator = new LIVR.Validator(loaded); ``` ```ts [Zod] const schema = z.object({ username: z.string().min(3).max(50), email: z.email(), role: z.enum(['admin', 'user', 'guest']), }); // It is code: a tree of class instances with methods. // It cannot be saved to a database or sent to another service, // and a Python or Go service cannot use it. ``` ::: Zod, Valibot and Joi build schemas from function calls. Zod and Valibot can export a JSON Schema for documentation, but the schema they validate with is still JavaScript code. fastest-validator and Ajv take schemas as objects, but compile them into JavaScript source, which is a problem of its own, see [below](#safe-with-untrusted-schemas). ## What you can do with data **Keep rules outside the code.** Rules that change often, like limits per customer or fields of a form, can live in a database, a config file or a CMS, and change without a deploy: ```js // Each tenant has its own limits, edited in an admin panel const { schema } = await db.query('SELECT schema FROM validation_rules WHERE tenant_id = ?', [ tenantId, ]); const validator = new LIVR.Validator(JSON.parse(schema)); ``` Creating a validator from such rules for every request costs less than a microsecond, and LIVR is the fastest validator in this case too: see [Rules built at runtime](/why/performance#rules-built-at-runtime). **Embed rules in larger configurations.** A form builder can keep the UI and the validation of a field in one JSON document, an API gateway can keep the validation next to the route: ```json { "route": "/api/users", "method": "POST", "auth": "jwt", "validation": { "email": ["required", "email"], "password": ["required", { "min_length": 8 }] } } ``` **Use the same rules on every side.** Send the schema to the browser and validate a form before it is submitted, with exactly the rules and error codes of the server. If the server is written in Perl, PHP, Python, Go, Java, Ruby or Erlang, it uses the same schema through its [own implementation](https://livr-spec.org/introduction/implementations.html), and returns the same error codes. See [Proven](/why/proven) for how the specification keeps them in agreement. **Generate and transform rules.** A schema is an object, so a program can build it, merge two of them, diff two versions or show them in a UI. No parser and no AST needed. ## Safe with untrusted schemas Some validators get their speed by compiling a schema into JavaScript source and running it with `new Function()`. If a schema can come from outside, from a database, a tenant or an API, a value in it can become code. This happened to fastest-validator ([issue #326](https://github.com/icebob/fastest-validator/issues/326)): ```js const Validator = require('fastest-validator'); const v = new Validator(); // The attacker controls the schema, for example through a database or an API const check = v.compile({ id: { type: 'number', max: 'console.log(process.env) || 999' }, }); check({ id: 123 }); // runs console.log(process.env) ``` LIVR treats every rule argument as data. The same attack does nothing: ```js const validator = new LIVR.Validator({ id: ['required', { max_number: 'console.log(process.env) || 999' }], }); validator.validate({ id: 123 }); // { id: 123 }: the argument is only compared with the value, as a number. Nothing is executed ``` The worst a hostile schema can do is reject valid data or let invalid data through, which is the job of whoever controls the rules anyway. One thing to keep in mind: `like` takes a regular expression, and a crafted pattern can be very slow on some inputs (ReDoS). If end users write schemas, review or restrict their `like` patterns. This matters for multi-tenant products where customers define their own rules, no-code tools and form builders, and any service that loads schemas from a database or another service. ## Your vocabulary, as data Aliases combine rules under a domain name with a domain error code. An alias is data too, so a whole vocabulary can come from a config file: ```json { "aliases": [ { "name": "product_sku", "rules": ["required", { "like": "^[A-Z]{3}-\\d{6}$" }], "error": "WRONG_SKU" }, { "name": "adult_age", "rules": ["positive_integer", { "min_number": 18 }], "error": "MUST_BE_ADULT" } ] } ``` ```js const validator = new LIVR.Validator({ sku: 'product_sku', age: ['required', 'adult_age'] }); config.aliases.forEach(alias => validator.registerAliasedRule(alias)); ``` ## And still typed For schemas in your code, add `as const` and TypeScript infers the type of the validated data from the data itself: ```ts const schema = { email: ['required', 'email'], role: { one_of: ['admin', 'user'] }, } as const; type User = InferFromSchema; // { email: string; role?: 'admin' | 'user' } ``` A schema loaded at runtime has no compile-time type, as with any validator. Declare the type of its output where you load it. See the [TypeScript guide](/typescript). ## The trade-off There is no fluent API, and a schema cannot hold an inline function such as `.refine(value => ...)`. Logic that rules cannot express goes into a named rule, registered in code: a [custom rule](/guide/custom-rules) of a few lines. The schema then refers to it by name and stays data. ## More * [Design decision #1: Schemas as data](/why/design-decisions#design-decision-1-schemas-as-data-vs-schemas-as-code) * [Design decision #2: No code generation](/why/design-decisions#design-decision-2-no-code-generation-security) * [Design decision #7: Rules aliasing](/why/design-decisions#design-decision-7-rules-aliasing-with-custom-error-codes) --- --- url: https://js.livr-spec.org/why/performance.md description: >- LIVR is the fastest JavaScript validator in the benchmarks, with rules fixed in code and with rules built at runtime, and it generates no code. --- # Performance LIVR is the fastest of the popular JavaScript validators in both benchmarks: with rules fixed in code, where a validator is created once and reused, and with rules built at runtime, where every call needs a new validator. It gets there without generating code, so its schemas stay [plain data](/why/schemas-are-data) that is safe to load from anywhere. > **Speed in both modes** is the priority → schemas are **compiled into closures once**, never into JavaScript source → no extra speed from code generation, and LIVR is still the fastest ## Rules fixed in code The usual case: a schema in your code, one validator for the lifetime of the process (static validation, hot start). ## Rules built at runtime Rules are often not known when the process starts: they live in a database and change without a deploy, differ per tenant or per customer plan, come from a form builder or a CMS, or are put together from the data of a request. Then every call needs a new validator (dynamic validation, cold start): ```js // The rules of a form are edited by the customer in an admin panel app.post('/forms/:formId', async (req, res) => { const rules = await db.getFormRules(req.params.formId); const validator = new LIVR.Validator(rules); // a new validator for every request const data = validator.validate(req.body); if (!data) return res.status(422).json(validator.getErrors()); // ... }); ``` A cache of validators helps only when the rules repeat, and it has to be invalidated when they change. With LIVR, creating a validator is so cheap that you can skip the cache. Validators that generate code pay for generating and compiling JavaScript again for every new schema, and here the gap grows to hundreds of times: ## All results LIVR in operations per second, the others as how many times they are slower than LIVR: ## How LIVR gets its speed **The schema is compiled once.** On the first validation, or when you call `prepare()`, every rule of the schema is turned into a small function: the rule factory receives its arguments, does the expensive work once (`like` builds its regular expression, `one_of` converts the allowed values to strings, or to a map for long lists) and returns a closure that only checks a value. Validation is a loop over these closures. Nothing is parsed or looked up again. **Creating a validator is cheap.** Building closures from a JSON structure costs far less than generating JavaScript source and compiling it with `new Function()`, which is what fastest-validator and Ajv do. That is why they are 100–130 and 780–950 times slower than LIVR when a validator is created per call. **The hot path allocates little.** All rules share one output array, the errors object is created only when there are errors, and metarules like `list_of` and `or` validate their items through [`getFieldRule()`](/api#validatorgetfieldrulefieldname), without building result and errors objects for every item. **The async validator stays sync until it has to.** `AsyncValidator` runs rules synchronously and waits only for a rule that returns a promise, so async validation of sync rules costs little more than sync validation. ## Fast enough for anything 3.5 million validations per second of a five-field form is about 300 billion validations a day on one CPU core. In a real service, the network, the database and the business logic take far longer than validation. So the point of the numbers is not that you need them, but that choosing data over code, and safety over code generation, costs nothing in speed. ## How the numbers were measured The benchmark lives in its own repository, [validators-benchmarks](https://github.com/koorchik/validators-benchmarks), with the code of every scenario. The charts on this site are its latest published results. * **Simple**: a form of five fields: required strings, an email, a positive integer, an enum and a minimum length * **Nested**: an object with two levels of nested objects and a list of strings * The schemas of all validators are equivalent: the benchmark checks that all of them accept and reject the same data, and unknown fields are removed from the output * Every validation gets a new input object, because fastest-validator and Ajv change their input * Every result is an average of repeated runs Absolute numbers depend on the hardware, so compare the ratios. To check them on your machine: ```bash git clone https://github.com/koorchik/validators-benchmarks cd validators-benchmarks npm install npm run bench ``` ## More * [Design decision #3: Optimized for both dynamic and static validation](/why/design-decisions#design-decision-3-optimized-for-both-dynamic-and-static-validation) * [Design decision #2: No code generation](/why/design-decisions#design-decision-2-no-code-generation-security) --- --- url: https://js.livr-spec.org/why/type-inference.md description: >- TypeScript infers the type of the validated data from a LIVR schema, which is plain data. No second definition, no code generator. --- # Type inference A LIVR schema is plain data, and TypeScript still knows exactly what `validate()` returns. You describe the rules once, and the type of the validated data follows from them: there is no interface to write next to the schema and keep in sync, and no code generator to run. > **Type safety without a second definition** is the priority → **types are inferred from the schema data** → schemas in code need `as const`, and a schema loaded at runtime gets its type from wherever you declare it ## One definition, two results ```ts import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; const schema = { email: ['required', 'email'], role: { one_of: ['admin', 'user'] }, address: { nested_object: { city: ['required', 'string'], zip: 'positive_integer', }, }, tags: { list_of: 'string' }, } as const; type User = InferFromSchema; // { // email: string; // role?: 'admin' | 'user'; // address?: { city: string; zip?: number }; // tags?: string[]; // } const validator = new LIVR.Validator(schema); const user = validator.validate(input); // User | false if (user) { user.address?.city; // string | undefined } ``` The same object is the validation rules at runtime and the source of the type at compile time. Change a rule, and the type changes with it. [Open this example in the TypeScript Playground](https://www.typescriptlang.org/play/?#code/PTAEBUAsFNQBQDYEMCeBzATgewK4DsATUAYxmIGsBnUAFxlpQAdpqs8EUAuUASxuoBK+UACMcNGmxJI8eLDVAIsSInkYBbUIyQUkaFop7lYCHgDcMAKB7rGWDAoAyASQBqA0ADNsmgOSmLXwBua1t7BRomWABvUGc8T2gMADEfAGVSaHUkUABfLx9Qf3MMYEjmSmDLSxBQAEFQF3dQSkzs3mpGZB48UAIkGiQAOlAAAyRqYjZKGlHFaH4IKIyMHkYFHsSMAqxNPkspvBmWtpyAXlBoy1BQLKQeBG4AbV8MaABHHB43gl8AGiKdwevgAun9rqBsAhoNxYmxoAB9LCeZ6+FTqHr-Io4ShJUF5cE3FQEN6USiwiE3PAsGjQAhIkQAK2gxBoFJuHJIfC4oBeb0+3zpWN8M1WeDQoMJnNAAC81txfHZKHxzIierT9Bh-pSCRDclLBmhyZdDDMkSiiqKehLdfkJiRpjQQjUwAAJLBmJKgACquO2kha0FgdFgmySPwYzEs5Vgvq9F3iW1SuwyMGyAB4Y8iTmmkAA+Z2HY5mJCmfqSbYXakAd0abgEQ1cpZ45fs6bjGDzAApWrmAJQhIsKHHx0AlssDexDcctgbQLuxIGPIoyPAAAWgAA8kLZoUMpuosVCYSuCBi8FjDcaXozKiC8gPqjxPKAuyOMH3LhD30NiaTKAA-Pu3JBKAtRWuKoAAD6gPgBDQJ4PR0pY+TQAguJfjcRZYHuShoF2M6thgQz6DQACiGDYBglBdn2j65JYQA) and hover over the types. ## What gets inferred * **Required and optional fields.** Fields are optional unless they have `required` or `default`. * **The type of every rule.** `email`, `string` and `like` give `string`, numeric rules give `number`, `list_of` gives an array, `nested_object` an object, `or` and `variable_object` a union. * **Literal types.** `one_of: ['admin', 'user']` gives `'admin' | 'user'`, `eq: 'active'` gives `'active'`. * **Defaults.** `{ default: 0 }` makes the field required and gives `number`. * **Types through the rule list.** Rules that check a value keep the type of earlier rules: `[{ one_of: ['admin', 'user'] }, { max_length: 10 }]` stays `'admin' | 'user'`. * **Aliases and your own rules,** once their type is declared, also when they replace a built-in rule. The [built-in rules page](/rules) lists the type of every rule, and the [TypeScript guide](/typescript) has all the details. ## Your own rules are typed too A custom rule declares its type once, by module augmentation, and every schema that uses it gets the right type: ```ts import type { As } from 'livr/types/inference'; declare module 'livr/types/inference' { interface RuleTypes { to_date: Date; // converts the value to a Date uuid: As; // checks a string } } const schema = { id: ['required', 'uuid'], created_at: ['required', 'to_date'], } as const; type Entity = InferFromSchema; // { id: string; created_at: Date } ``` Rules whose type depends on their arguments, aliases, meta rules and rule packages like [livr-extra-rules](https://www.npmjs.com/package/livr-extra-rules) work the same way, and any number of packages can add their rules. See [Custom rules type inference](/typescript#custom-rules-type-inference). ## Data and types together Other validators make you choose: schemas as code with inferred types, or schemas as data without them. | Validator | Schema is | Type of the validated data | | --- | --- | --- | | **LIVR** | **plain data** | **inferred from the schema (`InferFromSchema`)** | | Zod | code | inferred (`z.infer`) | | Valibot | code | inferred (`InferOutput`) | | Ajv | JSON Schema or JTD data | inferred for JTD (`JTDDataType`); for JSON Schema you write the type, and Ajv checks the schema against it | | Joi | code | not built in | | fastest-validator | object with code generation | not built in | With LIVR, the schema stays data that you can store, send and share with other languages, and TypeScript still types it. ## The trade-off Inference needs the schema literal in your code, with `as const`. A schema that arrives at runtime, from a database or an API, has no compile-time type, as with any validator. Declare its output type where you load it, and pass it to `new LIVR.Validator(schema)`. ## More * [TypeScript guide](/typescript) * [Rules are just data](/why/schemas-are-data) --- --- url: https://js.livr-spec.org/why/zero-dependencies.md description: >- livr has no runtime dependencies. About 2 KB for the core and 13 KB with all 68 built-in rules, min+gzip. --- # Zero dependencies `npm install livr` installs one package: `livr`. It has no runtime dependencies, so there is nothing to audit but LIVR itself, and nothing in a dependency tree that can break, be abandoned or be compromised. > **Supply-chain safety and a small footprint** are the priority → **no runtime dependencies** → every rule is implemented in LIVR itself, not taken from a library ## Why it matters **Supply-chain attacks go through dependencies.** Validation sits on the path of every request, so a compromised package there sees all your input. Popular packages have been hijacked before: [event-stream](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident) got code that targeted a bitcoin wallet app, [ua-parser-js](https://github.com/faisalman/ua-parser-js/issues/536) got a cryptominer and a password stealer, and the author of [colors and faker](https://www.bleepingcomputer.com/news/security/dev-corrupts-npm-libs-colors-and-faker-breaking-thousands-of-apps/) broke them on purpose. A package without dependencies can only be attacked directly. **Audits stay short.** Security reviews and compliance checks (SOC 2, ISO 27001, HIPAA) cover every package you ship. With LIVR, that is one package with readable, unminified source. **Nothing changes under you.** No upstream breaking change, no version conflict with another library, no deprecation warning from a transitive package. The version 2 API has been stable since 2016. ## Dependencies of popular validators | Validator | Version | Runtime dependencies | | --- | --- | --- | | **LIVR** | 2.12.0 | **0** | | Zod | 4.6.7 | 0 | | Valibot | 1.5.0 | 0 | | fastest-validator | 1.19.1 | 0 | | Ajv | 8.20.0 | 4: fast-deep-equal, fast-uri, json-schema-traverse, require-from-string | | Joi | 18.3.0 | 7: six `@hapi/*` packages and `@standard-schema/spec` | Zero dependencies alone is not unique. LIVR is the one that combines it with schemas as data and with the speed of the [benchmarks](/why/performance). ## Size Measured with esbuild (`--bundle --minify`) and gzip, version 2.13.0: | What you import | min+gzip | | --- | --- | | `livr/lib/Validator`, the core without rules | ~2 KB | | `livr`, the core and all 68 built-in rules (35 of LIVR 2.0, 33 added in LIVR 2.5) | ~13 KB | | `livr/async`, the async validator and all rules | ~14 KB | The 35 rules of LIVR 2.0 with the core are about 5 KB, imported one by one. The package ships its source as it is, without a build step, so you can read in `node_modules` exactly the code you run. To ship only the rules you use, import them one by one, see [Smaller bundles](/guide/usage#smaller-bundles). ## Runs anywhere LIVR uses no Node.js APIs and needs no polyfills: it runs in Node.js, Deno, Bun, browsers, web workers and edge runtimes such as Cloudflare Workers. In a browser, load it through any bundler or straight from an ESM CDN: ```html ``` ## The trade-off Every rule is written in LIVR, so there is no `validator.js` or `date-fns` behind `email` or `iso_date`. The built-in rules are the ones of the [specification](https://livr-spec.org/validation-rules.html), and more rules come from [livr-extra-rules](https://www.npmjs.com/package/livr-extra-rules), which has no dependencies either, or from your own [custom rules](/guide/custom-rules). ## More * [Design decision #4: Zero dependencies](/why/design-decisions#design-decision-4-zero-dependencies) --- --- url: https://js.livr-spec.org/why/proven.md description: >- Maintained since 2013, built on the cross-language LIVR specification and its shared test suite, which collects edge cases found in every implementation. --- # Proven by 13 years and a spec A validator is a dependency you keep for years: your schemas, error codes and UI translations depend on it. LIVR has been around since 2013, and its behavior is not defined by this code alone, but by a written specification and a test suite shared by implementations in 11 languages. > **Predictable, cross-language behavior** is the priority → a **formal specification with a shared JSON test suite** → the specification has to be maintained, and a new rule needs agreement before it lands ## 13 years | Year | | | --- | --- | | 2012 | Work on the [LIVR specification](https://livr-spec.org) starts | | 2013 | The JavaScript implementation starts in February, first npm release in December | | 2016 | Specification 2.0 and `livr` 2.0.0, on the same day. Version 2 is still the current one | | 2023 | 2.7: camelCase rule names, faster validation, 3–5× faster validator construction | | 2025 | 2.8 and 2.9: TypeScript declarations, then type inference from the schema | | 2026 | 2.11: fixes found by new test suite cases, and the fastest results in the [benchmarks](/why/performance). 2.13: the 33 rules added in LIVR 2.5 | 43 releases on npm, all of version 2 since 2016: a schema written for `livr` 2.0 still works with the latest release. ## One specification The [specification](https://livr-spec.org) describes every rule: its arguments, what it accepts, what it returns and which error code it gives. Because a LIVR schema is [data](/why/schemas-are-data), the specification can be language independent, and the same schema means the same thing in every implementation: JavaScript (this library), Perl, PHP, Python, Go, Java, Ruby, Erlang (two implementations), Lua, Raku and Swift. The [implementations page](https://livr-spec.org/introduction/implementations.html) links to all of them. A browser form validated by this library and a backend in Python or Go return the same error codes for the same data. ## One test suite, edge cases from every language The specification comes with a [test suite](https://livr-spec.org/how-to-contribute/test-suite.html) of JSON files: a schema, an input, and the expected output or errors. Implementations run these files in their own tests, and this library runs them for both the sync and the async validator. When an edge case is found in any implementation, it is added to the suite, so every other implementation gets checked against it too. Examples from the history of the suite: * **Fields named `constructor`, `toString` or `hasOwnProperty`.** In JavaScript, data objects inherit these names from `Object.prototype`, which caught bugs in this library. Now every implementation is tested with them. * **A trailing line break.** In Perl, PHP, Python and Ruby, `$` in a regular expression also matches before a final line break, so `"user@example.com\n"` could pass `email`. The suite now checks `email`, `url` and `iso_date` for it. * **Leap years and impossible dates** in `iso_date`, `"0.0"` in `positive_decimal`, numeric strings like `"10"` and `"1e1"` in `equal_to_field`, the output type of `one_of`, and empty values in `or` and aliases. Fixing a bug in one language makes all of them better. A new implementation starts with 13 years of found edge cases on day one. ## Checked in public The specification site runs every implementation against the test suite every week and publishes the results in its [conformance table](https://livr-spec.org/introduction/implementations.html). The current version of this library passes all 70 test cases. ## More * [Design decision #5: Formal specification with shared test suite](/why/design-decisions#design-decision-5-formal-specification-with-shared-test-suite) * [The LIVR specification](https://livr-spec.org) * [Changelog](/changelog) --- --- url: https://js.livr-spec.org/why/design-decisions.md --- # LIVR: Design Decisions This document examines LIVR's design decisions, compares them to alternatives in other validators, and explains why each choice matters. Every design decision follows a simple formula: > **Quality X** is a priority → We choose **design Y** → Accepting **trade-off Z** *** ## Quick Start ```typescript import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; const schema = { email: ['required', 'email'], age: ['required', 'positiveInteger'], role: { oneOf: ['admin', 'user'] as const }, } as const; type User = InferFromSchema; // Inferred: { email: string; age: number; role: 'admin' | 'user' } const validator = new LIVR.Validator(schema); const result = validator.validate({ email: 'user@example.com', age: '25', // String from form → automatically becomes number role: 'admin', hackAttempt: true, // Unknown field → automatically stripped }); if (result) { console.log(result.role); // TypeScript knows: 'admin' | 'user' } // result: { email: 'user@example.com', age: 25, role: 'admin' } ``` **What just happened?** * Type inference from schema via `InferFromSchema<>` * String `'25'` automatically coerced to number `25` * Unknown `hackAttempt` field automatically stripped * Result is fully typed as `User` > **Note**: Examples use JavaScript's camelCase naming convention (`positiveInteger`, `oneOf`). The [LIVR specification](https://livr-spec.org) uses underscore notation (`positive_integer`, `one_of`). The JavaScript implementation supports both. *** ## Cool Things You Can Do ### Store Validation Rules in a Database ```javascript // Schema is just JSON - store it anywhere const schema = JSON.parse(await db.getSchema('user-registration')); const validator = new LIVR.Validator(schema); ``` ### Share Rules Across Languages The same schema works in JavaScript, Python, Java, Ruby, and 7 more languages: ```json { "email": ["required", "email"], "age": ["positiveInteger"] } ``` ```python # Python validator = LIVR.Validator(schema) ``` ```javascript // JavaScript const validator = new LIVR.Validator(schema); ``` ### Create Domain-Specific Rules ```javascript validator.registerAliasedRule({ name: 'productSku', rules: ['required', { like: '^[A-Z]{3}-\\d{6}$' }], error: 'INVALID_PRODUCT_SKU', // Semantic error code }); // Now use it like a built-in rule const schema = { sku: 'productSku' }; ``` ### Validate with Async Rules ```javascript LIVR.Validator.registerDefaultRules({ uniqueEmail: () => async (email) => { const exists = await db.users.findByEmail(email); if (exists) return 'EMAIL_ALREADY_TAKEN'; }, }); ``` ### Validate Arrays ```javascript const schema = { tags: { listOf: 'string' }, items: { listOf: { nestedObject: { id: ['required', 'positiveInteger'], name: 'string', }, }, }, }; // Input: { tags: ["js", "node"], items: [{ id: 1, name: "Item" }] } ``` ### Safe with User-Provided Schemas ```javascript // LIVR schemas are data, not code - safe to load from untrusted sources const schema = JSON.parse(req.body.schema); const validator = new LIVR.Validator(schema); // No code execution risk ``` *** ## Table of Contents 1. [Quick Start](#quick-start) 2. [Cool Things You Can Do](#cool-things-you-can-do) 3. [Feature Comparison Matrix](#feature-comparison-matrix) 4. [Design Decisions](#design-decisions) * [#1: Schemas as Data vs Schemas as Code](#design-decision-1-schemas-as-data-vs-schemas-as-code) * [#2: No Code Generation (Security)](#design-decision-2-no-code-generation-security) * [#3: Optimized for Both Dynamic AND Static Validation](#design-decision-3-optimized-for-both-dynamic-and-static-validation) * [#4: Zero Dependencies](#design-decision-4-zero-dependencies) * [#5: Formal Specification with Shared Test Suite](#design-decision-5-formal-specification-with-shared-test-suite) * [#6: Error Codes with Mirrored Structure](#design-decision-6-error-codes-with-mirrored-structure) * [#7: Rules Aliasing with Custom Error Codes](#design-decision-7-rules-aliasing-with-custom-error-codes) * [#8: Rules as Transformers](#design-decision-8-rules-as-transformers-not-just-validators) * [#9: "Everything is a Rule" Architecture](#design-decision-9-everything-is-a-rule-architecture) * [#10: Input Immutability](#design-decision-10-input-immutability) * [#11: Unknown Fields Stripped by Default](#design-decision-11-unknown-fields-stripped-by-default) 5. [TypeScript Integration](#typescript-integration) 6. [Performance Summary](#performance-summary) *** ## Feature Comparison Matrix Before diving into design decisions, here's a comprehensive comparison of LIVR against popular alternatives: | Feature | LIVR | Zod | Joi | fastest-validator | Valibot | | ------------------------------- | -------------------------- | ----------------- | -------------- | ----------------- | ------------------ | | **TypeScript Inference** | ✅ `InferFromSchema<>` | ✅ `z.infer<>` | ❌ No | ❌ No | ✅ `InferOutput<>` | | **Async Validation** | ✅ AsyncValidator | ⚠️ Via parseAsync | ✅ Yes | ✅ Yes | ⚠️ Via pipeAsync | | **Custom Rules** | ✅ Very Easy | ✅ `.refine()` | ✅ `.custom()` | ✅ Yes | ✅ `v.custom()` | | **Rules Aliasing** | ✅ `registerAliasedRule()` | ❌ No | ❌ No | ⚠️ Partial | ❌ No | | **Schema Serializable** | ✅ Pure JSON | ❌ Code | ❌ Code | ⚠️ Partial | ❌ Code | | **Multi-Language Support** | ✅ 11 languages | ❌ JS/TS only | ❌ JS only | ❌ JS only | ❌ JS/TS only | | **Error Format** | Codes (i18n-ready) | Messages | Messages | Messages | Messages | | **Error Structure** | Mirrors input | Flat array | Flat array | Flat array | Flat array | | **Type Coercion** | ✅ Automatic | Manual | ✅ Via options | ✅ Yes | Manual | | **Unknown Fields** | Strip (default) | Strip (default) | Pass through | Pass through | Strip (default) | | **Input Immutability** | ✅ Always | ✅ Always | ✅ Always | ⚠️ Mutates | ✅ Always | | **Whole Package (min+gzip)** | 13 KB | 92 KB | 55 KB | 12.4 KB | 15 KB | | **Zero Dependencies** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes | | **Safe with Untrusted Schemas** | ✅ Yes | N/A | N/A | ❌ RCE risk | N/A | Whole package sizes include everything a package exports; Valibot and Zod are built for tree-shaking, and so is LIVR. See [Zero Dependencies](#design-decision-4-zero-dependencies) for how they were measured. *** ## Design Decisions ### Design Decision #1: Schemas as Data vs Schemas as Code > **Portability & Composability** is a priority → > We choose **pure JSON data structures** for schemas → > Accepting **no fluent API or method chaining** #### Comparison | Validator | Approach | Serializable | Embeddable in Config | Multi-Language | | ----------------- | ---------------- | ------------ | -------------------- | -------------- | | **LIVR** | JSON data | ✅ Yes | ✅ Yes | ✅ Yes | | Zod | Function chains | ❌ No | ❌ No | ❌ No | | Valibot | Pipe composition | ❌ No | ❌ No | ❌ No | | Joi | Fluent builder | ❌ No | ❌ No | ❌ No | | fastest-validator | Object literals | ⚠️ Partial | ⚠️ Partial | ❌ No | #### Example **LIVR** - Schema is pure data: ```javascript // Can be stored in DB, sent over API, loaded from config file const schema = { username: ['required', { minLength: 3 }, { maxLength: 50 }], email: ['required', 'email'], role: { oneOf: ['admin', 'user', 'guest'] }, }; // Store in database await db.saveSchema('user-registration', JSON.stringify(schema)); // Load and validate const loadedSchema = JSON.parse(await db.getSchema('user-registration')); const validator = new LIVR.Validator(loadedSchema); ``` **Zod** - Schema is code: ```typescript // Cannot serialize - this is JavaScript code, not data const schema = z.object({ username: z.string().min(3).max(50), email: z.string().email(), role: z.enum(['admin', 'user', 'guest']), }); // Cannot store in database // Cannot send over API // Cannot share with Python/Java services ``` #### Why This Matters: Embedding in Larger Configurations Because LIVR schemas are data, they can be embedded within larger configuration systems: **Form Builder** - Single JSON payload with UI + validation: ```json { "formId": "contact-form", "fields": [ { "name": "email", "type": "text", "label": "Email Address", "placeholder": "you@example.com", "validation": ["required", "email"] }, { "name": "age", "type": "number", "label": "Age", "validation": ["positiveInteger", { "numberBetween": [18, 120] }] } ] } ``` **API Gateway Config** - Route definition with validation: ```json { "route": "/api/users", "method": "POST", "auth": "jwt", "rateLimit": { "requests": 100, "window": "1m" }, "validation": { "body": { "email": ["required", "email"], "password": ["required", { "minLength": 8 }] } } } ``` **CMS Content Type** - Field definitions with validation: ```json { "contentType": "BlogPost", "fields": { "title": { "type": "string", "required": true, "validation": ["required", { "maxLength": 200 }] }, "slug": { "type": "string", "validation": ["required", { "like": "^[a-z0-9-]+$" }] } } } ``` This is impossible with code-based validators—you cannot embed JavaScript class instances in JSON configurations. #### Advanced Use Cases **Database-Driven Validation:** ```javascript // Database table: validation_schemas // | entity_type | schema | version | // |-------------|-------------------------------------------|------------| // | user | {"name": ["required"], "email": "email"} | 3 | // | product | {"sku": "productSku", "price": "..."} | 5 | async function validateEntity(entityType, data) { const { schema } = await db.query( 'SELECT schema FROM validation_schemas WHERE entity_type = ?', [entityType] ); const validator = new LIVR.Validator(JSON.parse(schema)); return validator.validate(data); } ``` **Multi-Tenant SaaS Validation:** ```javascript // Each tenant has customized validation requirements const tenantSchemas = { 'tenant-acme': { order: { amount: ['required', { maxNumber: 10000 }], // Conservative limit approver: 'required', // Requires approval }, }, 'tenant-bigcorp': { order: { amount: ['required', { maxNumber: 1000000 }], // Higher limit // No approver required }, }, }; function validateForTenant(tenantId, entityType, data) { const schema = tenantSchemas[tenantId]?.[entityType]; if (!schema) throw new Error('Unknown entity type'); return new LIVR.Validator(schema).validate(data); } ``` **Safe Schema Loading:** LIVR schemas are just data—safe to load from untrusted sources: ```javascript // SAFE: Loading LIVR schema from user input const userSchema = JSON.parse(req.body.schema); const validator = new LIVR.Validator(userSchema); // Just data, no code execution // DANGEROUS: fastest-validator with untrusted schema (real RCE vulnerability) const Validator = require('fastest-validator'); const v = new Validator(); const check = v.compile(req.body.schema); // RCE if schema contains malicious code! // Also dangerous: Using eval with any schema (obvious security risk) // const zodSchema = eval(req.body.schema); // Never eval untrusted input! ``` LIVR schemas cannot contain executable code. The worst a malicious schema can do is define invalid rules (which LIVR will reject) or create overly complex validations. *** ### Design Decision #2: No Code Generation (Security) > **Security** is a priority → > We choose **rule parameters as data, never executed** → > Accepting **no speedups from code generation (static speed is still ahead of code-gen validators)** #### Comparison | Validator | Approach | Code Injection Risk | Safe with Untrusted Schemas | | ----------------- | ------------------- | ------------------------ | --------------------------- | | **LIVR** | Data interpretation | ❌ Impossible | ✅ Yes | | fastest-validator | Code generation | ⚠️ RCE vulnerability | ❌ No | | Zod | Runtime evaluation | ❌ No (not serializable) | N/A | | Joi | Runtime evaluation | ❌ No (not serializable) | N/A | #### The fastest-validator RCE Vulnerability fastest-validator achieves high static performance by compiling schemas to JavaScript code. This creates a dangerous attack vector when schema parameters aren't properly sanitized. **Real vulnerability** ([GitHub Issue #326](https://github.com/icebob/fastest-validator/issues/326)): ```javascript const Validator = require('fastest-validator'); const v = new Validator(); // Attacker controls schema (e.g., from database, API, user input) const maliciousSchema = { id: { type: 'number', max: 'console.log(process.env) || 999', // Injected code! }, }; const check = v.compile(maliciousSchema); check({ id: 123 }); // Executes: console.log(process.env) - RCE! ``` #### Why LIVR is Immune LIVR treats schema parameters as data, never as code: ```javascript const LIVR = require('livr'); // Even with malicious input, no code execution const schema = { id: ['required', { maxNumber: 'console.log(process.env) || 999' }], }; const validator = new LIVR.Validator(schema); validator.validate({ id: 123 }); // Result: { id: 123 } // No code execution - the argument is only compared with the value, as a number ``` #### When This Matters * **Multi-tenant SaaS**: Different customers define their own validation rules * **No-code platforms**: Users build forms with custom validation * **Database-stored schemas**: Schemas loaded from potentially compromised sources * **API-driven validation**: Schemas received from external services *** ### Design Decision #3: Optimized for Both Dynamic AND Static Validation > **Balanced performance** is a priority → > We choose **lightweight JSON parsing + efficient rule execution** → > Accepting **no extra speed from code generation (LIVR is still the fastest in the benchmarks below)** #### Dynamic Validation Performance Dynamic validation measures schema creation + validation—critical for runtime-generated schemas. | Validator | Simple (ops/sec) | Nested (ops/sec) | vs LIVR | | ----------------- | ---------------- | ---------------- | ------------------- | | **LIVR** | **1,364,823** | **634,178** | baseline | | Valibot | 921,293 | 534,161 | 1.2-1.5x slower | | Joi | 41,255 | 23,150 | 27-33x slower | | Zod | 15,650 | 8,649 | 73-87x slower | | fastest-validator | 10,697 | 5,946 | 107-128x slower | | Ajv | 1,442 | 804 | **788-946x slower** | LIVR leads dynamic validation, Valibot is 1.2-1.5x slower. LIVR schemas are plain JSON data, so creating a validator only builds a closure per rule. fastest-validator and Ajv generate and compile JavaScript code for every schema, which is the slowest option here. #### Static Validation Performance Static validation measures pre-compiled validator speed—typical production usage. | Validator | Simple (ops/sec) | Nested (ops/sec) | Note | | ----------------- | ---------------- | ---------------- | ------------------- | | **LIVR** | **3,457,657** | **1,764,838** | Data interpretation | | Valibot | 2,876,819 | 1,535,356 | | | fastest-validator | 2,857,396 | 1,173,430 | Code generation | | Zod | 2,794,647 | 1,248,719 | | | Ajv | 2,513,133 | 1,270,083 | Code generation | | Joi | 313,462 | 175,847 | | LIVR is the fastest: Valibot, fastest-validator and Zod are 1.15-1.5x slower, Ajv is about 1.4x slower, Joi about 10x. #### The Key Insight **3.4 million ops/sec is already far beyond any real-world need.** To put this in perspective: * 3.4M ops/sec = validating every request for a service handling 300 billion requests/day * Most applications handle thousands, not millions, of validations per second * Network latency, database queries, and business logic dominate actual response times LIVR is the fastest in both dynamic and static validation here, without generating code. #### How These Numbers Were Measured Measured with the separate [validators-benchmarks](https://github.com/koorchik/validators-benchmarks) repository (`npm run bench -- --runs 2`, with `LIVR_DIR` pointing to this repository). Node.js 26, AMD Ryzen 9 5950X; Zod 4.6, Valibot 1.5, Joi 18.3, fastest-validator 1.19, Ajv 8.20. Average of two runs, the runs differ by up to 6%. * **Simple**: a 5-field form (required strings, email, positive integer, enum, min length) * **Nested**: an object with two levels of nested objects and a list of strings * Schemas are equivalent across validators (the benchmark checks that all of them accept and reject the same data), and unknown fields are stripped * A new input object is created for every validation (fastest-validator and Ajv mutate their input), for all validators Absolute numbers depend on hardware; compare the ratios. #### When Dynamic Performance Matters * **Form builders**: Schemas constructed at runtime based on configuration * **Multi-tenant validation**: Different rules per customer, loaded dynamically * **A/B testing**: Different validation strictness across variants * **API gateways**: Route-specific validation loaded from config *** ### Design Decision #4: Zero Dependencies > **Supply chain security & lightweight deployment** is a priority → > We choose **zero runtime dependencies** → > Accepting **implementing each rule without reusing existing libraries** #### Comparison | Validator | Zero Deps | Runtime Deps | Whole Package (min+gzip) | | ----------------- | --------- | ----------------------------- | ------------------------ | | **LIVR** | Yes | 0 | 13 KB (core: 2 KB) | | Valibot | Yes | 0 | 15 KB | | Zod | Yes | 0 | 92 KB | | fastest-validator | Yes | 0 | 12.4 KB | | Ajv | No | 4 | 38 KB | | Joi | No | 7 (six @hapi/\* packages) | 55 KB | Sizes are of everything a package exports, bundled with esbuild (`--minify`) and gzipped: LIVR 2.12.0, Valibot 1.5, Zod 4.6, fastest-validator 1.19, Ajv 8.20, Joi 18.3. Valibot and Zod are built for tree-shaking, so a typical schema needs only part of them. LIVR can be tree-shaken too: import the core (`livr/lib/Validator`) and only the rules you use. #### Why Zero Dependencies Matters **Supply chain security:** * Every dependency is a potential attack vector * Transitive dependencies multiply risk exponentially * Notable incidents: [event-stream](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident) (bitcoin theft), [colors.js](https://www.bleepingcomputer.com/news/security/dev-corrupts-npm-libs-colors-and-faker-breaking-thousands-of-apps/) (intentional corruption), [ua-parser-js](https://github.com/faisalman/ua-parser-js/issues/536) (cryptominer injection) * Joi's 7 dependencies (six of them @hapi/\* packages) and Ajv's 4 each represent audit surface **Audit complexity:** * Zero dependencies = only your code to audit * No transitive dependency trees to review * Simpler security compliance (SOC2, HIPAA, etc.) **Stability:** * No breaking changes from upstream packages * No version conflicts in dependency resolution * No abandoned dependency risk #### Additional Benefits: Minimal Bundle Size **Serverless/Edge computing:** * Cold start time directly affected by bundle size * Edge functions have strict size limits (Cloudflare Workers: 1MB) * Smaller bundles = faster deployments **Frontend applications:** * Every KB affects page load time * Validation often needed client-side for UX * 13 KB vs 55 KB is significant for mobile users **Embedded systems:** * IoT devices have limited memory * Validation needed at the edge *** ### Design Decision #5: Formal Specification with Shared Test Suite > **Cross-platform consistency** is a priority → > We choose **formal spec (livr-spec.org) with JSON test suite** → > Accepting **specification maintenance overhead** #### Comparison | Validator | Has Specification | Shared Test Suite | Language Implementations | | ----------------- | ----------------------------------------- | ----------------- | ------------------------ | | **LIVR** | ✅ [livr-spec.org](https://livr-spec.org) | ✅ JSON files | 11 languages | | Zod | ❌ No | ❌ No | JS/TS only | | Joi | ❌ No | ❌ No | JS only | | fastest-validator | ❌ No | ❌ No | JS only | | Valibot | ❌ No | ❌ No | JS/TS only | #### Why a Formal Specification is Possible Because LIVR schemas are data (JSON), not code: * The specification is language-agnostic * Test cases are JSON files any implementation can run * No need to port JavaScript code to other languages #### Available Implementations The same schema works identically across all implementations: | Language | Package | | --------------------- | ------------------------------------------------------------------------------------------ | | JavaScript/TypeScript | [livr](https://www.npmjs.com/package/livr) | | Perl | [Validator::LIVR](https://metacpan.org/pod/Validator::LIVR) | | PHP | [validator/livr](https://packagist.org/packages/validator/livr) | | Python | [LIVR](https://pypi.org/project/LIVR/) | | Go | [go-livr](https://pkg.go.dev/github.com/k33nice/go-livr) | | Java | [validator-livr](https://central.sonatype.com/artifact/io.github.vlbaluk/validator-livr) | | Ruby | [ruby-livr](https://rubygems.org/gems/ruby-livr) | | Erlang | [liver](https://hex.pm/packages/liver), [olifer](https://github.com/Prots/olifer) | | Lua | [lua-livr](https://luarocks.org/modules/fperrad/lua-livr) | | Raku | [LIVR](https://raku.land/cpan:KOORCHIK/LIVR) | | Swift | [Livr](https://cocoapods.org/pods/Livr) | The [implementations page](https://livr-spec.org/introduction/implementations.html) of the specification shows how many test suite cases each of them passes. #### Benefits 1. **Polyglot microservices**: Share validation logic across services in different languages 2. **Frontend/backend consistency**: Same rules in browser (JS) and server (Python/Java/etc.) 3. **Mobile validation**: Identical rules in iOS, Android, and web 4. **Community growth**: Anyone can create a new implementation with confidence *** ### Design Decision #6: Error Codes with Mirrored Structure > **i18n & Domain clarity** is a priority → > We choose **error codes in a structure mirroring input** → > Accepting **less verbose error details (no rule names, expected values, etc.)** #### Comparison | Validator | Error Format | Structure | Focus | i18n Approach | | ----------------- | --------------------- | ------------- | -------------- | ------------------------------- | | **LIVR** | Codes (`WRONG_EMAIL`) | Mirrors input | Domain meaning | Codes (separate translation) | | Zod | Messages + metadata | Flat array | Implementation | Messages (40+ built-in locales) | | Joi | Messages + context | Flat array | Implementation | Messages (manual setup) | | Valibot | Issues array | Flat array | Implementation | Messages (i18n package) | | fastest-validator | Messages | Flat array | Implementation | Messages (manual setup) | **Why LIVR's approach is different**: All validators offer some form of message customization, but LIVR is the only one that returns **error codes** instead of messages. This creates a clean separation of concerns: * **LIVR**: Validator returns codes (`WRONG_EMAIL`) → UI layer translates to localized messages * **Others**: Translation logic is baked into the validator via message customization With LIVR, you maintain a single translation table mapping codes to messages. Other validators require configuring translations within the validation layer itself. For example: * **Valibot**: The [@valibot/i18n](https://valibot.dev/guides/internationalization/) package provides translations, but you're translating rule-specific messages—you cannot define custom domain error codes * **Zod**: [Error customization](https://zod.dev/error-customization) allows custom messages and has 40+ built-in locales, but LIVR's aliasing approach (`registerAliasedRule` with custom error codes) is more powerful for domain-driven validation #### Example **Input:** ```javascript const input = { user: { email: 'not-an-email', profile: { age: 'not-a-number', }, }, }; ``` **LIVR errors** - Mirrors input structure: ```javascript { user: { email: 'WRONG_EMAIL', profile: { age: 'NOT_INTEGER' } } } ``` **Other validators** - Flat array with paths: ```javascript [ { path: ['user', 'email'], message: 'Invalid email format', code: 'invalid_string', }, { path: ['user', 'profile', 'age'], message: 'Expected number, received string', code: 'invalid_type', }, ]; ``` #### Why Mirrored Structure Matters **Easy field mapping**: Error structure matches input structure exactly. Display errors next to fields trivially: ```javascript // LIVR - direct property access const emailError = errors.user?.email; // 'WRONG_EMAIL' const ageError = errors.user?.profile?.age; // 'NOT_INTEGER' // Others - must search array const emailError = errors.find((e) => e.path.join('.') === 'user.email')?.message; ``` #### Domain-Focused vs Implementation-Focused LIVR tells you **WHAT** is wrong (domain meaning): ``` WRONG_EMAIL - The email is invalid TOO_SHORT - The value is too short INVALID_PRODUCT_SKU - The SKU doesn't match our format ``` Others tell you **WHICH rule** failed (implementation detail): ``` "Invalid email format" "String must contain at least 8 character(s)" "Invalid input: expected string to match pattern /^[A-Z]{3}-\d{6}$/" ``` #### i18n Benefits Error codes map cleanly to translations: ```javascript // translations/en.json { "WRONG_EMAIL": "Please enter a valid email address", "TOO_SHORT": "Must be at least {min} characters", "INVALID_PRODUCT_SKU": "SKU format: ABC-123456" } // translations/es.json { "WRONG_EMAIL": "Por favor ingrese un correo electrónico válido", "TOO_SHORT": "Debe tener al menos {min} caracteres", "INVALID_PRODUCT_SKU": "Formato SKU: ABC-123456" } ``` With message-based validators, translations are scattered across validator configurations or require complex message extraction. #### Clean Separation of Concerns Validation logic belongs in the business layer. Message formatting belongs in the presentation layer. LIVR enforces this separation: ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Validation │───▶│ Error Codes │───▶│ Presentation │ │ Layer │ │ (WRONG_EMAIL) │ │ Layer │ │ (LIVR) │ │ │ │ (Messages) │ └─────────────────┘ └──────────────────┘ └─────────────────┘ ``` Benefits: * **Backend doesn't dictate UI text**: Same API serves web, mobile, and third-party clients * **Messages can change without code changes**: Update translation files, not validators * **Different clients, different messages**: Mobile app might show shorter messages than web *** ### Design Decision #7: Rules Aliasing with Custom Error Codes > **Domain-driven validation** is a priority → > We choose **rule aliasing with custom error code override** → > Accepting **indirection (alias → underlying rules)** #### Comparison | Validator | Rule Aliasing | Custom Error Codes | Domain Vocabularies | | ----------------- | ----------------------- | -------------------- | ------------------- | | **LIVR** | `registerAliasedRule()` | Via `error` property | Yes | | fastest-validator | Limited | No | No | | Zod | No | Via message only | No | | Joi | No | Via message only | No | | Valibot | No | Via message only | No | #### Creating Domain Vocabularies Aliases compose existing rules into domain-specific validators: ```javascript const validator = new LIVR.Validator(schema); // E-commerce domain rules validator.registerAliasedRule({ name: 'productSku', rules: ['required', { like: '^[A-Z]{3}-\\d{6}$' }], error: 'INVALID_PRODUCT_SKU', // Custom error code! }); validator.registerAliasedRule({ name: 'priceUsd', rules: ['required', 'positiveDecimal', { maxNumber: 999999.99 }], error: 'INVALID_PRICE', }); validator.registerAliasedRule({ name: 'quantity', rules: ['required', 'positiveInteger', { maxNumber: 10000 }], error: 'INVALID_QUANTITY', }); // Clean, domain-focused schema const orderSchema = { sku: 'productSku', price: 'priceUsd', qty: 'quantity', }; ``` #### Healthcare Domain Example ```javascript validator.registerAliasedRule({ name: 'patientMrn', rules: ['required', { lengthEqual: 10 }, { like: '^MRN\\d{7}$' }], error: 'INVALID_MEDICAL_RECORD_NUMBER', }); validator.registerAliasedRule({ name: 'diagnosisCode', rules: ['required', { like: '^[A-Z]\\d{2}\\.?\\d{0,4}$' }], error: 'INVALID_ICD10_CODE', }); validator.registerAliasedRule({ name: 'npiNumber', rules: ['required', { lengthEqual: 10 }, 'integer'], error: 'INVALID_NPI', }); const patientSchema = { mrn: 'patientMrn', diagnosis: 'diagnosisCode', provider: 'npiNumber', }; ``` #### Alias Composition Aliases can reference other aliases, enabling hierarchical rule composition: ```javascript // Base rules validator.registerAliasedRule({ name: 'nonEmptyString', rules: ['required', 'string', 'trim', { minLength: 1 }], }); // Domain rules built on base rules validator.registerAliasedRule({ name: 'personName', rules: ['nonEmptyString', { maxLength: 100 }], error: 'INVALID_NAME', }); validator.registerAliasedRule({ name: 'companyName', rules: ['nonEmptyString', { maxLength: 200 }], error: 'INVALID_COMPANY_NAME', }); // Higher-level aliases building on previous ones validator.registerAliasedRule({ name: 'contactEmail', rules: ['required', 'email', 'toLc'], error: 'INVALID_CONTACT_EMAIL', }); ``` This layered approach enables: * **DRY principle**: Common patterns defined once * **Consistency**: All name fields share the same base validation * **Easy maintenance**: Update base rule, all dependent rules benefit #### Configuration-Driven Aliases Since aliases are defined with plain objects, load them from configuration: ```json { "aliases": [ { "name": "productSku", "rules": ["required", { "like": "^[A-Z]{3}-\\d{6}$" }], "error": "INVALID_SKU" }, { "name": "priceUsd", "rules": ["required", "positiveDecimal", { "maxNumber": 999999.99 }], "error": "INVALID_PRICE" } ] } ``` ```javascript const config = JSON.parse(fs.readFileSync('rules-config.json')); config.aliases.forEach((alias) => validator.registerAliasedRule(alias)); ``` This enables: * Non-developers defining validation rules via config files * Different rule sets per environment * A/B testing validation strictness * Multi-tenant rule customization *** ### Design Decision #8: Rules as Transformers (Not Just Validators) > **Single-pass processing & web form compatibility** is a priority → > We choose **rules that validate AND transform in one pass** → > Accepting **implicit transformation (less explicit than separate transform step)** #### Comparison | Validator | String Transformers | Type Coercion | Approach | | ----------------- | ------------------------------- | ----------------------------- | ------------------ | | **LIVR** | `trim`, `toLc`, `toUc` | Automatic (safe) | Single pass | | Zod | `trim()`, `toLowerCase()`, etc. | `z.coerce.*` (explicit) | Pipe/chain | | Valibot | `trim`, `toLowerCase`, etc. | `toNumber()`, etc. (explicit) | Pipe | | Joi | `.trim()`, `.lowercase()`, etc. | `convert: true` (default) | Chain | | fastest-validator | `trim`, `lowercase` | `convert: true` | Options (mutates!) | #### Example: Web Form Data HTML forms submit everything as strings. LIVR handles this seamlessly: ```javascript const validator = new LIVR.Validator({ age: ['required', 'positiveInteger'], price: ['required', 'decimal'], name: ['required', 'trim'], email: ['required', 'email', 'toLc'], }); // Input from web form (all strings) const input = { age: '25', price: '19.99', name: ' John Doe ', email: 'JOHN@EXAMPLE.COM', }; const result = validator.validate(input); // Output (properly typed): // { // age: 25, // number (coerced from string) // price: 19.99, // number (coerced from string) // name: 'John Doe', // string (trimmed) // email: 'john@example.com' // string (lowercased) // } ``` #### Built-in Transformers | Rule | Effect | | ----------- | ---------------------------------- | | `trim` | Remove leading/trailing whitespace | | `toLc` | Convert to lowercase | | `toUc` | Convert to uppercase | | `default` | Provide default value if missing | | `remove` | Remove field from output | | `leaveOnly` | Keep only specified fields | #### Type Coercion Numeric rules automatically coerce string inputs: | Rule | Input | Output | | ----------------- | -------- | ------ | | `integer` | `"42"` | `42` | | `positiveInteger` | `"100"` | `100` | | `decimal` | `"3.14"` | `3.14` | | `numberBetween` | `"50"` | `50` | **Reference**: [LIVR Type Coercing Specification](https://livr-spec.org/validation-rules/types-coercing.html) #### Secure Coercion: LIVR vs Valibot's Old Approach LIVR's numeric coercion is **secure by design**. Before transforming a value, it validates the input type: ```javascript // LIVR internal implementation if (!util.isPrimitiveValue(value)) return 'FORMAT_ERROR'; value += ''; // Only coerce AFTER validating it's a primitive ``` This prevents dangerous coercions where complex types masquerade as valid strings: | Input | Old Valibot `coerce` | LIVR | | --------------- | ----------------------------- | ------------------------- | | `{}` | `"[object Object]"` (passes!) | `FORMAT_ERROR` (rejected) | | `[]` | `""` (passes!) | `FORMAT_ERROR` (rejected) | | `[1,2,3]` | `"1,2,3"` (passes!) | `FORMAT_ERROR` (rejected) | | `"42"` (string) | `"42"` (valid) | `42` (number, valid) | Valibot [removed implicit coercion](https://valibot.dev/blog/valibot-v1.2-release-notes/) in v1.2 due to this security concern, now requiring explicit `toNumber()`, `toString()` etc. LIVR has always followed the secure pattern: **validate type first, then transform**. *** ### Design Decision #9: "Everything is a Rule" Architecture > **Extensibility & Consistency** is a priority → > We choose **minimal core where built-in and custom rules use identical API** → > Accepting **no special optimizations for built-in rules** #### Comparison | Validator | Core Size | Custom Rule API | Replace Built-ins | | ----------------- | --------- | ------------------------ | ----------------- | | **LIVR** | ~4KB | Same as built-in | ✅ Yes | | Zod | ~12KB | Different (`.refine()`) | ❌ No | | Joi | ~150KB | Different (`.custom()`) | ❌ No | | Valibot | ~1KB | Different (`v.custom()`) | ❌ No | | fastest-validator | ~40KB | Different | ❌ No | #### The Uniform Rule API Every rule in LIVR—built-in or custom—follows the same pattern: ```javascript ruleName: (ruleArg) => (value, allValues, outputArr) => { // Return error code if invalid if (invalid) return 'ERROR_CODE'; // Optionally transform value if (transform) outputArr.push(transformedValue); }; ``` #### Creating Custom Rules **Simple validation rule:** ```javascript LIVR.Validator.registerDefaultRules({ strongPassword: () => (value) => { if (!value) return; // Let 'required' handle empty if (value.length < 8) return 'TOO_SHORT'; if (!/[A-Z]/.test(value)) return 'MISSING_UPPERCASE'; if (!/[0-9]/.test(value)) return 'MISSING_NUMBER'; if (!/[!@#$%^&*]/.test(value)) return 'MISSING_SPECIAL'; }, }); // Use like any built-in rule const schema = { password: ['required', 'strongPassword'] }; ``` **Rule with parameter:** ```javascript LIVR.Validator.registerDefaultRules({ minAge: (minAge) => (value) => { if (value === undefined || value === null) return; if (value < minAge) return 'TOO_YOUNG'; }, }); // Use with parameter const schema = { age: ['required', 'positiveInteger', { minAge: 18 }] }; ``` **Rule with transformation:** ```javascript LIVR.Validator.registerDefaultRules({ toDate: () => (value, _, outputArr) => { if (!value) return; const date = new Date(value); if (isNaN(date.getTime())) return 'INVALID_DATE'; outputArr.push(date); // Transform string to Date object }, }); const schema = { birthDate: ['required', 'toDate'] }; // Input: { birthDate: '1990-05-15' } // Output: { birthDate: Date object } ``` #### Why "Everything is a Rule" Matters 1. **First-class custom rules**: Your rules are not second-class citizens 2. **Replace built-ins**: Don't like how `email` works? Replace it 3. **Consistent debugging**: All rules work the same way 4. **Predictable API**: One pattern to learn, one pattern to document 5. **Transparent implementation**: Inspect code of complex meta rules (like `nestedObject`, `listOf`) and use them as examples for your own rules #### Async Validation The uniform rule API extends to async rules. Use `AsyncValidator` for rules that need database lookups or API calls: ```javascript import LIVR from 'livr/async'; // Use async version // Register an async rule LIVR.Validator.registerDefaultRules({ uniqueEmail: () => async (email) => { if (!email) return; const exists = await db.users.findByEmail(email); if (exists) return 'EMAIL_ALREADY_TAKEN'; }, }); // Use like any other rule const validator = new LIVR.AsyncValidator({ email: ['required', 'email', 'uniqueEmail'], }); // Validate returns a Promise const result = await validator.validate({ email: 'test@example.com' }); ``` No special syntax—async rules follow the same pattern, just return a Promise. #### Rule Isolation for Multi-Tenancy Register rules per-validator instance to prevent cross-tenant rule pollution: ```javascript // Tenant A's validator with custom rules const tenantAValidator = new LIVR.Validator(schemaA); tenantAValidator.registerRules({ tenantAFormat: () => (value) => { if (!value) return; if (!value.startsWith('ACME-')) return 'INVALID_FORMAT'; }, }); // Tenant B's validator - cannot use tenantAFormat const tenantBValidator = new LIVR.Validator(schemaB); // tenantBValidator doesn't have access to tenantAFormat rule ``` This enables secure multi-tenant validation where each tenant can have custom rules without affecting others. *** ### Design Decision #10: Input Immutability > **Predictability & Functional style** is a priority → > We choose **always return new object, never mutate input** → > Accepting **memory allocation overhead (negligible in practice)** #### Comparison | Validator | Input Mutation | Predictable | Safe Reuse | | ----------------- | ------------------------------------ | ----------- | ---------- | | **LIVR** | ❌ Never | ✅ Yes | ✅ Yes | | fastest-validator | ⚠️ Mutates with `$$strict: 'remove'` | ❌ No | ❌ No | | Zod | ❌ Never | ✅ Yes | ✅ Yes | | Joi | ❌ Never | ✅ Yes | ✅ Yes | | Valibot | ❌ Never | ✅ Yes | ✅ Yes | #### Example **LIVR - Input preserved:** ```javascript const input = { name: ' John ', email: 'JOHN@EXAMPLE.COM', extraField: 'will be stripped', }; const result = validator.validate(input); // Result (new object): // { name: 'John', email: 'john@example.com' } // Input UNCHANGED: console.log(input.extraField); // 'will be stripped' - still there! console.log(input.name); // ' John ' - still has whitespace! ``` **fastest-validator - Input mutated:** ```javascript const input = { name: 'John', extraField: 'will be removed', }; const result = check(input); // With $$strict: 'remove' // Input MODIFIED: console.log(input.extraField); // undefined - gone! ``` #### Why Immutability Matters 1. **Predictable behavior**: Functions shouldn't have hidden side effects 2. **Safe reuse**: Validate same input with multiple validators 3. **Debugging**: Original input preserved for logging/inspection 4. **Functional programming**: Fits naturally with immutable data patterns *** ### Design Decision #11: Unknown Fields Stripped by Default > **Security by default** is a priority → > We choose **always strip unknown fields** → > Accepting **must explicitly allow extra fields if needed** #### Comparison | Validator | Default Behavior | Security | Configuration Needed | | ----------------- | ---------------- | --------- | ---------------------------------- | | **LIVR** | Strip unknown | ✅ Secure | None | | Zod | Strip unknown | ✅ Secure | `.passthrough()` to allow | | Joi | Pass through | ⚠️ Risk | `.options({ stripUnknown: true })` | | fastest-validator | Pass through | ⚠️ Risk | `$$strict: 'remove'` | | Valibot | Strip unknown | ✅ Secure | Use `passthrough` to allow | #### Example ```javascript const validator = new LIVR.Validator({ username: 'required', email: 'email', }); const input = { username: 'john', email: 'john@example.com', isAdmin: true, // Attacker-injected role: 'superuser', // Attacker-injected __proto__: { hack: 1 }, // Prototype pollution attempt }; const result = validator.validate(input); // { username: 'john', email: 'john@example.com' } // Malicious fields stripped automatically ``` #### Security Benefits 1. **Mass assignment protection**: Extra fields can't sneak into your data model 2. **Privilege escalation prevention**: Fields like `isAdmin` or `role` are stripped 3. **Prototype pollution defense**: Dangerous properties removed 4. **Defense in depth**: Security by default, not by configuration #### Safe Defaults Summary LIVR's design choices consistently favor security: | Aspect | LIVR Default | Why It's Secure | | ----------------------- | ------------------------ | ------------------------------ | | Unknown fields | Stripped | Mass assignment protection | | Missing optional fields | Omitted from output | No unexpected nulls/undefineds | | Invalid rules | Rejected at compile time | Fail-fast, no silent failures | | Rule isolation | Supported per-instance | Multi-tenant security | | Schema parameters | Data, never code | No code injection possible | *** ## TypeScript Integration LIVR fully supports TypeScript type inference using the `InferFromSchema<>` utility: ```typescript import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; const userSchema = { id: ['required', 'positiveInteger'], username: ['required', { minLength: 3 }, { maxLength: 50 }], role: { oneOf: ['admin', 'user', 'guest'] as const }, settings: { nestedObject: { theme: { oneOf: ['light', 'dark'] as const }, fontSize: 'positive_integer', }, }, } as const; // 'as const' required for literal type inference type User = InferFromSchema; // Inferred type: // { // id: number; // username: string; // role?: 'admin' | 'user' | 'guest'; // settings?: { // theme?: 'light' | 'dark'; // fontSize?: number; // } // } const validator = new LIVR.Validator(userSchema); const input = getUserInput(); // data from API, form, etc. const result = validator.validate(input); if (result) { // result is typed as User console.log(result.role); // TypeScript knows: 'admin' | 'user' | 'guest' | undefined } ``` ### Custom Rules with Type Inference Register custom rule types via module augmentation: ```typescript import LIVR from 'livr'; import type { InferFromSchema } from 'livr/types'; import type { As } from 'livr/types/inference'; // Declare custom rule types via module augmentation declare module 'livr/types/inference' { interface RuleTypes { toDate: Date; // converts the value to a Date uuid: As; // checks a string } } // Register the rules LIVR.Validator.registerDefaultRules({ toDate: () => (value, _, outputArr) => { if (LIVR.util.isNoValue(value)) return undefined; const date = new Date(value); if (isNaN(date.getTime())) return 'INVALID_DATE'; outputArr.push(date); return undefined; }, uuid: () => (value) => { if (LIVR.util.isNoValue(value)) return undefined; const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidRegex.test(value)) return 'INVALID_UUID'; return undefined; }, }); const schema = { id: ['required', 'uuid'], createdAt: ['required', 'toDate'], } as const; type Entity = InferFromSchema; // { id: string; createdAt: Date } ``` *** ## Performance Summary ### Dynamic Validation (Schema Creation + Validation) Best for: Runtime-generated schemas, form builders, multi-tenant validation | Validator | Simple | Nested | Relative | | ----------------- | --------- | ------- | --------------- | | **LIVR** | 1,364,823 | 634,178 | **1x** | | Valibot | 921,293 | 534,161 | 1.2-1.5x slower | | Joi | 41,255 | 23,150 | 27-33x slower | | Zod | 15,650 | 8,649 | 73-87x slower | | fastest-validator | 10,697 | 5,946 | 107-128x slower | | Ajv | 1,442 | 804 | 788-946x slower | ### Static Validation (Pre-compiled) Best for: Fixed schemas, API endpoints, production validation | Validator | Simple | Nested | Note | | ----------------- | --------- | --------- | --------------- | | **LIVR** | 3,457,657 | 1,764,838 | | | Valibot | 2,876,819 | 1,535,356 | | | fastest-validator | 2,857,396 | 1,173,430 | Code generation | | Zod | 2,794,647 | 1,248,719 | | | Ajv | 2,513,133 | 1,270,083 | Code generation | | Joi | 313,462 | 175,847 | | See [How These Numbers Were Measured](#how-these-numbers-were-measured). ### The Bottom Line * **Dynamic validation**: the fastest: 1.2-1.5x faster than Valibot, 27-33x faster than Joi, 73-87x faster than Zod, 107-128x faster than fastest-validator, 788-946x faster than Ajv * **Static validation**: the fastest: 1.15-1.5x faster than Valibot, fastest-validator and Zod, 1.4x faster than Ajv, 10x faster than Joi * **Balance**: the fastest in both modes, without code generation *** ### Resources * **Specification**: [livr-spec.org](https://livr-spec.org) * **GitHub**: [github.com/koorchik/LIVR](https://github.com/koorchik/LIVR) * **NPM**: [npmjs.com/package/livr](https://www.npmjs.com/package/livr) * **Extra Rules**: [livr-extra-rules](https://www.npmjs.com/package/livr-extra-rules)