A tiny, composable validation library. Bueno primary aims to be an
improvement on form validation libraries like
yup
and
superstruct
, but
can also be used as a lightweight API validation library. You'll like
it if you need something
🌳 Small & tree-shakeable.
💡 Expressive! Use full boolean logic to compose your schemas
💫 Bidirectional. Learn more
🚀 Awesome error messages in multiple languages supported out of the box, with more on the way. Learn more
⏱ Asynchronous (when needed!)
You can check out bueno
directly in the browser in this
jsfiddle.
Install using npm install --save bueno
or yarn add bueno
.
Check out the quickstart section below, or go directly to the API docs
- Usage example: Vanilla JS form
- Usage example:
express
- Usage example:
react
+formik
- Customizing error messages
bueno
allows you to quickly and predictably compose validation
schemas. Here's how it looks in action:
import { alphaNumeric, atLeast, check, checkPerKey, deDE, describePaths, either, email, enUS, length, moreThan, not, number, object, optional, string, svSE } from 'bueno'
const username =
string(length(atLeast(8)), alphaNumeric)
const age =
number(atLeast(18), not(moreThan(130)))
const user = object({
id: either(email, username),
age: optional(age)
})
const input = {
id: 'philip@example.com',
age: 17
}
console.log(check(input, user, enUS))
// 'Age must be at least 18 or left out'
console.log(check(input, user, describePaths(svSE, [['age', 'Ålder']])))
// 'Ålder måste vara som minst 18'
console.log(checkPerKey(input, user, deDE))
// { age: 'Muss mindestens 18 sein' }
Schemas are constructed using basic
schemas like number
string
, atLeast(10)
, exactly(null)
and
by using combinators like either
,
object
, array
, fix
to create more complex schemas.
Most schemas (specifically Schema_
:s) can be called as
functions with other schemas as arguments. E.g.
number(even, atLeast(10))
The semantics are a schema returning the value of number
with the
additional validations from even
and atLeast(10)
taking place.
check • checkPerKey • result
The following functions allow you to feed input into a schema to parse
& validate it. Note that schema evaluation is cached, so calling e.g.
check(input)
then immediately result(input)
is not inefficient.
checkAsync :: <A>(value : A, schema : Schema<A, any>, locale : Locale) : string | null
Returns a string
with a validation error constructed using the given
locale, or null
if validation succeeded.
check('123', number, enUS)
// 'Must be a number'
Returns an object of errors for each key in an object (for a schema
constructed using the object
combinator)
checkByKey({ n: '123', b: true }, object({ n: number, b: boolean }, enUS)
// { n: 'Must be a number', b: 'Must be a boolean' }
Returns the result of parsing using a schema.
result({ n: '123', d: 'null' }, object({ n: toNumber, d: toJSON })
// { n: 123, d: null }
The async versions of check
, checkByKey
and result
respectively.
apply • both • compact • defaultTo • either • every • fix • flip • not • object • optional • pipe • self • setMessage • some • when
Combinators create new, more complex schemas out of existing, simpler schemas.
Creates a schema that satisfies both of its arguments.
both :: <A, B, C, D>(v : Schema<A, C>, w : Schema<B, D>,) => Schema_<A & B, C & D>
const schema =
both(even, atLeast(10))
check(schema, 11, enUS)
// 'Must be even.'
check(schema, 8, enUS)
// 'Must be at least 10.'
check(schema, 12, enUS)
// null
You may prefer using the call signatures of schemas over using this combinator.
Creates a schema that satisfies either of its arguments.
either :: <A, B, C, D>(v : Schema<A, C>, w : Schema<B, D>,) => Schema_<A & B, C & D>
const schema =
either(even, atLeast(10))
check(schema, 11, enUS)
// null
check(schema, 8, enUS)
// null
check(schema, 9, enUS)
// 'Must be even or at least 10'
Make a schema also match undefined
.
optional :: <A>(v : Schema<A, B>) : Schema<A | undefined, B | undefined>
const schema = optional(number)
check(schema, 9, enUS)
// null
check(schema, undefined, enUS)
// null
check(schema, null, enUS)
// 'Must be a number or left out
not :: <A, B>(v : Schema<A, B>) => Schema_<A, B>
Negates a schema. Note that negation only affect the "validation" and
not the "parsing" part of a schema. Essentially, remember that not
does not affect
the type signature of a schema.
For example, not(number)
is the same as just number
. The reason is
that we can't really do much with a value that we know only to have
type "not a number".
const schema =
number(not(moreThan(100)))
check(103, schema, enUS)
// Must not be more than 100
Create a schema on objects from an object of schemas.
object :: <AS, BS>(vs :
{ [Key in keyof AS]: Schema<AS[Key], any> } &
{ [Key in keyof BS]: Schema<any, BS[Key]> }
) => Schema_<AS, BS>
const schema = object({
age: number,
name: string
})
check({ age: 13 }, schema, enUS)
// Name must be a string
check({ age: '30', name: 'Philip' }, schema, enUS)
// Age must be a number
check({ age: 30, name: 'Philip' }, schema, enUS)
// null
You can use compact
to make undefined keys optional.
inexactObject
and exactObject
are versions of this that are more
lenient / strict w.r.t keys not mentioned in the schema.
Remove keys in an object that are undefined
.
compact :: <A, B>(p : Schema<A, B>) : Schema_<UndefinedOptional<A>, UndefinedOptional<B>> {
const schema =
compact(object({ n: optional(number) }))
result({ n: undefined }, schema)
// {}
Create a schema that can recursively be defined in terms itself. Useful for e.g. creating a schema that matches a binary tree or other recursive structures.
fix :: <A, B = A>(fn : (v : Schema<A, B>) => Schema<A, B>) => Schema_<A, B>
TypeScript is not too great at inferring types using this combinators, so typically help it using an annotation as below
type BinTree<A> = {
left : BinTree<A> | null,
right : BinTree<A> | null,
value : A
}
const bintree = fix<BinTree<string>, BinTree<number>>(bintree => object({
left: either(exactly(null), bintree),
right: either(exactly(null), bintree),
value: toNumber
}))
Create a schema dynamically defined in terms of its input.
type User = {
verified : boolean,
email : string | null
}
const schema = self<User, User>(user => {
return object({
verified: boolean,
email: user.verified ? email : exactly(null)
})
})
Reverse a schema
flip :: <A, B>(schema : Schema<A, B>) => Schema_<B, A>
const schema = reverse(toNumber)
result(123, schema)
// '123'
Set a default value for a schema when it fails parsing.
defaultTo :: <A, B>(b: B, schema : Schema<A, B>) => Schema_<B, A>
const schema =
defaultTo(100, number)
result(null, schema)
// 100
Pipe the output of a schema as the input into another
pipe :: <A, B, C>(s : Schema<A, B>, t : Schema<B, C>) => Schema_<A, C>
const schema =
pipe(toNumber, lift(x => x + 1))
result('123', schema)
// 124
Set the input of a schema to a fixed value. Can be used when creating a schema where the definition of one key depends on another.
apply :: <A>(v : Schema<A, A>, value : A, path : string) => Schema_<any, A>;
type Schedule = {
weekday : string
price : number
}
const schema = self((schedule : Schedule) => object({
weekday: oneOf('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'),
price: when(
// When schedule.weekday is Sat or Sun, the
// price must be at least 100, otherwise at most 50.
apply(oneOf('Sat', 'Sun'), schedule.weekday, 'weekday'),
atLeast(100),
atMost(50)
)
})
A variable arguments version of both
.
every :: <A, B>(...vs : Schema<A, B>[]) => Schema_<A, B>
Set the error message of a parser.
const thing = setMessage(
object({ foo: string, bar: number }),
l => l.noun('should be a thingy),
)
check({ foo: '' } as any, thing, enUS)
// 'Should be a thingy'
A variable arguments version of either
.
some :: <A, B>(...vs : Schema<A, B>[]) => Schema_<A, B>
"if-then-else" on parsers.
when :: <A, B>(cond : Schema<A, any>, consequent : Schema<A, B>, alternative : Schema<A, B>) => Schema_<A, B>
The "else" part is optional, in which case this combinator has the signature
when :: <A, B>(cond : Schema<A, any>, consequent : Schema<A, A>) => Schema_<A, A>
const schema =
when(even, atLeast(10), atMost(20))
check(8, schema, enUS)
// 'Must be at least 10 when even'
check(21, schema, enUS)
// 'Must be at most 20 when not even'
check(11, schema, enUS)
// null
alphaNumeric • any • atLeast • atMost • between • boolean • date • email • emptyString • even • exactly • id • integer • length • lessThan • lift • match • moreThan • number • objectExact • objectInexact • odd • oneOf • optionalTo • pair • path • size • string • sum • swap • toDate • toJSON • toNumber • toString • toURL • unknown
Basic schemas are simple schemas that can be composed into more complex ones using the combinator API.
alphaNumeric :: Schema_<string, string>
Match an alphanumeric string.
check('acb123', alphaNumeric, enUS)
// null
check('acb|123', alphaNumeric, enUS)
// Must have letters and numbers only
any :: Schema_<any, any>
Successfully matches any input.
check(123, any, enUS)
// null
check(undefined, any, enUS)
// null
atLeast :: <A>(lb : A) => Schema_<A, A>
Matches a value at least as big as the provided lower bound lb
.
const schema =
atLeast(100)
check(88, schema, enUS)
// 'Must be at least 100'
atMost :: <A>(ub : A) => Schema_<A, A>
Matches a value at most as big as the provided upper bound ub
.
const schema =
atMost(100)
check(88, schema, enUS)
// 'Must be at most 100'
Matches a value between the provided lower and upper bounds (inclusive)
between :: (lb : number, ub : number) => Schema_<number, number>
check(99, schema, enUS)
// 'Must be between 100 and 200'
check(201, schema, enUS)
// 'Must be between 100 and 200'
check(100, schema, enUS)
// null
check(200, schema, enUS)
// null
Matches a boolean.
boolean :: Schema_<boolean, boolean>
Matches a Date
object.
date :: Schema_<Date, Date>
Matches an email (validated using a permissive regular expression)
email :: Schema_<string, string>
Matches the empty string
emptyString :: Schema_<string, string>
Matches an even number
even :: Schema_<number, number>
Creates a schema that matches a single value, optionally using an equality comparison operator.
exactly :: <A>(target : A, equals : (x : A, y : A) => boolean = (x, y) => x === y) => Schema_<A, A>
check('abc', exactly('abc'), enUS)
// null
check('abd', exactly('abc'), enUS)
// 'Must be abc'
check('abd', exactly('abc', (x, y) => x.length === y.length), enUS)
// null
id :: <A>() => Schema_<A, A>
The identity schema that always succeeds. Unlike any
, id
can be provided a type
argument other than the any
type.
const schema =
object({ foo: id<number>() })
check({ foo: 123 }, schema, enUS)
// null
check({ foo: 'hi!' }, schema, enUS)
// evaluates to null, but has a type error
Match a whole number
integer :: Schema_<number, number>
Match an object with property length matching the schema argument
length :: <A extends { length : number }>(...vs : Schema<number, any>[]) => Schema_<A, A>
const username =
string(length(exactly(10)))
const items =
array(between(1, 10))
Match a number less than the provided upper bound ub
lessThan :: (ub : number) => Schema_<number, number>
Lift a function into a schema that uses the function for parsing.
const schema =
pipe(toNumber, lift(x => x + 1))
result('123', schema)
// 124
Match a string matching the provided regular expression.
const greeting =
match(/Hello|Hi|Hola/, m => m.mustBe('a greeting'))
check('Hello', greeting, enUS)
// null
check('Yo', greeting, enUS)
// 'Must be a greeting'
Match a number more than the provided lower bound lb
moreThan :: (lb : number) => Schema_<number, number>
Match any number
number :: Schema_<number, number>
Like object
but match the object exactly, i.e. error if additional
keys to the ones specified are present.
Like object
but match the object inexactly, i.e. whereas object
will silently remove any keys not specified in the schema,
objectInexact
will keep them.
Matches an odd number
odd :: Schema_<number, number>
Match exactly one of the given elements
const weekend =
oneOf('Fri', 'Sat', 'Sun'),
check('Sat', weekend, enUS)
// null
check('Wed', weekend, enUS)
// Must be Fri, Sat or Sun
Swap elements
swap :: <A>(dict : [[A, A]]) => Schema_<A, A>
const optionalToEmptyString =
swap([[undefined, ''], [null, '']])
result(null, optionalToEmptyString)
// ''
result(undefined, optionalToEmptyString)
// ''
result('foo', optionalToEmptyString)
// 'foo'
Map null
and undefined
to another value.
optionalTo :: <A>(to : A) => Schema<null | undefined | A, A>;
const schema =
pipe(optionalTo(''), length(atMost(3)))
result(null, schema)
// ''
check(null, schema, enUS)
// null'
check('123123', schema, enUS)
// 'Must have length at most 3.'
Create a schema for pairs or values from a pair of schemas (where a pair is a typed two-element array)
pair :: <A, B, C, D>(v : Schema<A, C>,w : Schema<B, D>) => Schema_<[A, B], [C, D]>
const schema =
pair(toNumber, toDate)
result(['123', '2019-12-12'], schema)
// [ 123, 2019-12-12T00:00:00.000Z ]
Set the path
that a schema reports errors at.
path :: <A, B>(path : string, v : Schema<A, B>) => Schema<A, B>
const schema =
path('foo', number)
check('', schema, enUS)
// 'Foo must be a number'
size
is the same as length
except using the size
property. Usable
for sets etc.
size :: <A extends { size : number }>(...vs : Schema<number, any>[]) => Schema_<A, A>
Match a string
string :: Schema_<string, string>
Match an array with sum matching the schema argument.
sum :: (...vs : Schema<number, any>[]) => Schema_<number[], number[]>
const schema =
sum(atLeast(10))
check([1,2,3], schema, enUS)
// Must have sum at least 10
Convert a string to a date. Simply parses the string using the date constructor which can be unreliable, so you may want to use date-fns instead.
toDate :: Schema_<string, Date>
Converts a string to JSON.
toJSON :: Schema_<string, any>
Converts a string to a number.
toNumber :: Schema_<string, number>
Converts a value to a string.
toString :: Schema_<string, string>
Converts a string to an URL object.
Successfully parses a value (same as any
) but types it as unknown.
array • iterable • map • set • toArray • toMap • toMapFromObject • toSet
Create a schema on arrays from a schema on its values.
array :: <A, B>(v : Schema<A, B>,) : Schema_<A[], B[]>
const schema =
array(toNumber)
result(['1', '2', '3'], schema)
// [1, 2, 3]
check(['1', '2', true], schema, enUS)
// Element #3 must be a string
Match any Iterable
value.
iterable :: <A>() => Schema_<Iterable<A>, Iterable<A>>
const schema =
iterable<string>()
check(['hello', 'world'], schema, enUS)
// null
Create a schema that matches a Map
from schemas describing the keys
and values respectively.
map :: <A, B, C, D>(k : Schema<A, C>, v : Schema<B, D>) => Schema_<Map<A, B>, Map<C, D>>
const schema =
map(number(atLeast(10)), string(length(atLeast(1))))
check(new Map([[1, 'a'], [2, 'b'], [3, 'c']]), schema, enUS)
// Element #1.key must be at least 10
check(new Map([[11, 'a'], [12, 'b'], [13, '']]), schema, enUS)
// Element #3.value must have length at least 1
check(new Map([[11, 'a'], [12, 'b'], [13, 'c']]), schema, enUS)
// null
Create a schema for a Set from a schema describing the values of the set.
set :: <A, B>(v : Schema<A, B>) => Schema_<Set<A>, Set<B>>
const schema =
set(any)
check(new Set([1, 'a', true], schema, enUS)
// null
check([1, 'a', true], schema, enUS)
// 'Must be a set'
const schema =
set(toNumber)
parse(new Set(['1', '2', '3']))
// Set(3) { 1, 2, 3 }
Convert an iterable to an array.
toArray :: <A>() => Schema_<Iterable<A>, A[]>
Convert an iterable of pairs to a Map
toMap :: <A, B>() => Schema_<Iterable<[A, B]>, Map<A, B>>
Convert an objet into a Map.
toMapFromObject :: <A extends symbol | string | number, B>() : Schema_<{ [key in A]: B }, Map<A, B>>
result({ 'a': 3, 'b': 10, 'c': 9 }, toMapFromObject())
// Map(3) { 'a' => 3, 'b' => 10, 'c' => 9 }
It only works on "real" objects.
check('', toMapFromObject(), enUS)
// 'Must be an object'
Convert an iterable of values into a set.
toSet :: <A>() : Schema_<Iterable<A>, Set<A>>
Factory functions let you create new schema definitions.
createSchema :: <A, B>(
parse : SchemaFactory<A, B>,
unparse : SchemaFactory<B, A> = irreversible('createSchema')
) : Schema_<A, B>
Create a schema from two "parser factories" for each "direction" of parsing. (See Bidirectionality.) A single factory may be provided, but the schema will not be invertible.
A SchemaFactory
is simply a function of type
type SchemaFactory<A, B> = (a : A) => Action<{
parse?: { ok : boolean, msg : string | Message },
validate?: { ok : boolean, msg : string | Message },
result?: B,
score?: number
}>
All properties are optional and described below.
Provide this if the schema performs any parsing. This is the result
value of the parsing. A schema performs parsing when it transforms the
input of the schema into something else, e.g. by transforming a string
representation of a date into a Date
-object.
Provide this if the schema is doing any parsing. (See result
)
The ok
parameter indicates whether the parse was
successful. message
is the error message describing what the parser does.
Provide this if the schema is doing any validation. The ok
parameter
indicates whether the validation was successful. message
is the
error message describing what the parser does.
The score is used by bueno
to generate better error messages in
certain situations. You're most likely fine not providing it.
You may however optionally proide a score
between 0 and 1 to
indicate how successful the schema was. This will by default be either
0 or 1 depending on whether the schema successfully handled its input
or not.
An example of a schema that uses a non-binary score is
array(number)
. If we ask this schema to handle the input
[1,2,3,4,'5']
it will be ranked with a score of 4/5.
Here's an example, creating a schema matching a "special" number.
const special = number(createSchema(
async function(a : number) {
// `createSchema` may be async.
await new Promise(k => setTimeout(k, 100))
return {
validate: {
ok: [2, 3, 5, 7, 8].indexOf(a) >= 0,
msg: (l : Builder) => l.mustBe('a special number')>
},
// Transform the special number into a string.
result: '' + a
}
}
))
The type of a schema. Converts a value of type A
into one of type B
and validates the result.
A Schema
that can be used with "call syntax". An example of a
Schema_
is number
, and it can be enhanced by calling it
with additional arguments.
const schema =
number(even, atLeast(12))
An Action<A>
is either an A
or a Promise<A>
. A schema returning a
Promise
will be asynchronous.
A builder is an object that contains methods for building error messages when using type-safe i18n. See customizing errors
This type is used to create error messages that are independent of a specific locale.
It is a value of type <Rep>(l : MessageBuilder<Rep>) => Rep
. I.e. it
uses a message builder to create a representation of an error
message. An example would be
<Rep>(l : MessageBuilder<Rep>) => l.mustBe('a thingy!')
(The 'a thingy!
is hard-coded to english here. We can extend the
grammar of MessageBuilder
to accommodate this. See Customzing error messages)