Javscript schema-based validation for forms
ok-form is a simple, predictable object schema validator that is optimized for validation of forms.
joi and yup are both good libraries, but can cause friction when used for validating forms. ok-form improves upon them by having:
- a smaller bundle size (3kB vs 20+kB)
- sensible default casting behavior
- a simple API for conditional validation and references (no magic strings or refs!)
// with npm
npm install ok-form
// with yarn
yarn add ok-form
import ok from 'ok-form';
const schema = ok.object({
email: ok.string().email('Invalid email'),
age: ok.number(),
password: ok.string().min(8, 'Password must be at least 8 characters!'),
confirmPassword: ok.string().test((v, { parent }) => {
if (v !== parent.password) return 'Passwords must match!';
}),
});
schema.validate({
email: '[email protected]',
age: 24,
password: 'supersecret',
confirmPassword: 'supersecret',
});
// -> { valid: true, errors: null }
schema.validate({
email: 'not an email',
age: 'not a number',
password: 'short',
confirmPassword: 'notsecret',
});
/*
->
{
valid: false,
errors: {
email: 'Invalid email',
age: 'Must be a number',
password: 'Password must be at least 8 characters!',
confirmPassword: 'Passwords must match!',
},
}
*/
The response from validate
and validateAsync
takes the shape:
{ valid: boolean, errors: any }
valid
: whether or not the value matches the schemaerrors
: The schema's errors, where each error message is positioned where the error occured (see below for example)
Validates a value using the schema.
const schema = ok.object({ foo: ok.number('Must be a number!') });
schema.validate({ foo: 5 }); // -> { valid: true, errors: null }
schema.validate({ foo: 'a' }); // -> { valid: false, errors: { foo: 'Must be a number!' }
Validates an asynchronous schema.
const schema = ok
.string()
.test(async v => (await emailInUse(v)) && 'Email already in use!');
schema.validateAsync('[email protected]'); // -> Promise<{ valid: true, errors: null }>
schema.validateAsync('[email protected]'); // -> Promise<{ valid: false, errors: 'Email already in use!'>
Attempts to cast a value using the schema. All transforms defined in the schema will be run, and the resulting object returned. If an "impossible" cast is attempted (e.g. casting a string to an object) the input object will be returned.
const schema = ok.object({ foo: ok.number('Must be a number!') });
schema.cast({ foo: 5 }); // -> { foo: 5 }
schema.cast({ foo: '5' }); // -> { foo: 5 }
schema.cast(null); // -> null
schema.cast(''); // -> ''
All of the any
methods can also be used on the more specific schema types, like string
or object
.
Create a schema with minimal default validation/transformations. If you want to implement all validation logic yourself, you can use this.
const schema = ok.any();
schema.validate(5); // -> { valid: true };
schema.validate(true); // -> { valid: true };
schema.validate({ foo: [1, 2, 3] }); // -> { valid: true };
Marks the schema as optional, meaning that ""
, null
, undefined
are considered valid.
const schema = ok.string().optional();
schema.validate(''); // -> { valid: true };
schema.validate(null); // -> { valid: true };
schema.validate(undefined); // -> { valid: true };
Fields are required by default. If you want to specify the error message for an empty value, you can use .required
to set it.
const schema = ok.string().required('This is required!');
schema.validate(''); // -> { valid: false, errors: 'This is required!' };
schema.validate(null); // -> { valid: false, errors: 'This is required!' };
Add a transformation to the schema. These transformations will be run when a value is cast via the schema.
The transformations will be run in the order they are defined.
All transformations will run before validation.
const schema = ok
.number()
.transform(v => v * 2)
.max(10);
schema.validate(8); // -> { valid: false };
schema.cast(8); // -> 16;
Adds a custom test function to the schema.
The test will be passed the value, and should return a non me string (the error message) if there is an issue, or a non string (or empty string) if the value is valid.
const schema = ok.string().test(v => {
if (v === 'evil') return 'No evil allowed';
});
schema.validate('evil'); // -> { valid: false, errors: 'No evil allowed' };
schema.cast('good'); // -> { valid: true };
The second argument to test
is the Context
object, which is used if you need to reference other fields.
Context an object of the shape { parent: Parent, root: Root, path: string[] }
parent
is the parent value of the current node, before transformation.
const schema = ok.object({
foo: ok
.string()
.test((v, { parent }) => console.log(`Value: ${v}, parent: ${parent}`)),
bar: ok.string(),
});
schema.validate({ foo: 'Foo!', bar: 'Bar!' });
// Value: Foo!, parent: { foo: 'Foo!', bar: 'Bar!' }
root
is the value passed to validate
, before transformation.
const schema = ok.object({
deep: ok.object({
nesting: ok.object({
foo: ok
.string()
.test((v, { root }) => console.log(`Value: ${v}, root: ${root}`)),
}),
}),
});
schema.validate({ deep: { nesting: { foo: 'Foo!' } } });
// Value: Foo!, root: { deep: { nesting: { foo: 'Foo!' } } }
path
is an array of strings of the path to the current node.
const schema = ok.object({
nested: ok.object({
array: ok.array(
ok
.string()
.test((v, { path }) => console.log(`Value: ${v}, path: ${path}`))
),
}),
});
schema.validate({ nested: { array: ['Foo!'] } });
// Value: Foo!, path: ['nested', 'array', '0']
Note that these tests will run even if the value is null, undefined, or empty, unlike the type specific tests (eg. max
, .length
, etc).
Create a schema for a string.
If the value is not null, undefined, or an object, the value will be cast using String
. You can override the default cast by passing a transformation function as the second argument.
const schema = ok.string();
schema.validate('hello'); // -> { valid: true };
schema.validate(5); // -> { valid: true };
schema.cast(5); // -> '5';
schema.validate({ foo: 5 }); // -> { valid: false };
Require the string be a certain length.
const schema = ok.string().length(5);
schema.validate('hello'); // -> { valid: true };
schema.validate('hello world'); // -> { valid: false };
Require the string be at least a certain length.
const schema = ok.string().min(5);
schema.validate('h'); // -> { valid: false };
schema.validate('hello'); // -> { valid: true };
schema.validate('hello world'); // -> { valid: true };
Require the string be at most a certain length.
const schema = ok.string().max(5);
schema.validate('h'); // -> { valid: true };
schema.validate('hello'); // -> { valid: true };
schema.validate('hello world'); // -> { valid: false };
Require the string match a regular expression.
const schema = ok.string().matches(/^[a-z]*$/);
schema.validate('hello'); // -> { valid: true };
schema.validate('Hello'); // -> { valid: false };
Require the string is an email address (using this regex).
const schema = ok.string().matches(/^[a-z]*$/);
schema.validate('[email protected]'); // -> { valid: true };
schema.validate('hello world'); // -> { valid: false };
Create a schema for a number.
If the value is a string, the value will be cast using Number
. You can override the default cast by passing a transformation function as the second argument.
const schema = ok.number();
schema.validate(5); // -> { valid: true };
schema.validate('5'); // -> { valid: true };
schema.cast('5'); // -> '5';
schema.validate('hello'); // -> { valid: false };
Require the number be at least a certain value.
const schema = ok.string().min(5);
schema.validate(1); // -> { valid: false };
schema.validate(5); // -> { valid: true };
schema.validate(10); // -> { valid: true };
Require the number be at most a certain value.
const schema = ok.string().max(5);
schema.validate(1); // -> { valid: true };
schema.validate(5); // -> { valid: true };
schema.validate(10); // -> { valid: false };
Require the number be less than a certain value.
const schema = ok.string().lessThan(5);
schema.validate(1); // -> { valid: true };
schema.validate(5); // -> { valid: false };
schema.validate(10); // -> { valid: false };
Require the number be more than a certain value.
const schema = ok.string().moreThan(5);
schema.validate(1); // -> { valid: false };
schema.validate(5); // -> { valid: false };
schema.validate(10); // -> { valid: true };
Require the number be positive
const schema = ok.string().positive();
schema.validate(-5); // -> { valid: false };
schema.validate(0); // -> { valid: false };
schema.validate(5); // -> { valid: true };
Require the number be negative
const schema = ok.string().negative();
schema.validate(-5); // -> { valid: true };
schema.validate(0); // -> { valid: false };
schema.validate(5); // -> { valid: false };
Require the number be an integer
const schema = ok.string().integer();
schema.validate(5); // -> { valid: true };
schema.validate(5.25); // -> { valid: false };
Create a schema for a boolean.
If the value is a string, the values true
and false
will be cast to their boolean representation. You can override the default cast by passing a transformation function as the second argument.
const schema = ok.boolean();
schema.validate(true); // -> { valid: true };
schema.validate('false'); // -> { valid: true };
schema.validate(5); // -> { valid: false };
Create a schema for an object.
Shape
is an object where each value is a schema.
const schema = ok.object({ foo: ok.number(); });
schema.validate({ foo: 5 }) // -> { valid: true };
schema.validate({ foo: 'hello' }) // -> { valid: false };
schema.validate(5) // -> { valid: false };
Create a schema for an array.
const schema = ok.array(ok.number());
schema.validate([1, 2, 3]); // -> { valid: true };
schema.validate(['hello', 'world']); // -> { valid: false };
schema.validate(5); // -> { valid: false };
Require the array be a certain length.
const schema = ok.array(ok.number()).length(2);
schema.validate([1, 2]); // -> { valid: true };
schema.validate([1, 2, 3]); // -> { valid: false };
Require the array be at least a certain length.
const schema = ok.array(ok.number()).min(2);
schema.validate([1]); // -> { valid: false };
schema.validate([1, 2]); // -> { valid: true };
schema.validate([1, 2, 3]); // -> { valid: true };
Require the array be at most a certain length.
const schema = ok.array(ok.number()).max(2);
schema.validate([1]); // -> { valid: true };
schema.validate([1, 2]); // -> { valid: true };
schema.validate([1, 2, 3]); // -> { valid: false };
Conditional types can be achieved using test. If you return a schema from .test
, it will be evaluated against the value. So if you only want to run the test under a certain condition, simply check the condition and return a schema for that case.
Example: a
and b
are only required if the other is set
const schema = ok.object({
// Using ternary
a: ok
.number()
.optional()
.test((_, { parent }) => (parent.b ? ok.number() : null)),
// Using short-circuiting
b: ok
.number()
.optional()
.test((_, { parent }) => parent.a && ok.number()),
});
schema.validate({ a: null, b: null }); // -> { valid: true };
schema.validate({ a: 1, b: null }); // -> { valid: false };
schema.validate({ a: null, b: 1 }); // -> { valid: false };
schema.validate({ a: 1, b: 1 }); // -> { valid: true };
ok-form supports typescript out of the box. All of the schema constructors take 3 generic paramaters:
ok.any<Input, Parent, Root>();
Input
is the type of the object that it expects to be passed to validate
and cast
. For objects, it is also used to infer the shape of the schema you should pass it.
Parent
is the type of the schema's Parent value
Root
is the type of the schema's Root value
Formik is a great tool for reducing form boilerplate. Here's how you would integrate ok-form with it:
const schema = ok.object({ name: ok.string(); email: ok.string() });
const form = () => (
<Formik
validate={values => schema.validate(values).errors || {}}
>
{/* `|| {}` is necessary because Formik expects an empty object instead */}
{/* of null if there are no errors */}
{/* Form code */}
</Formik>
)