Skip to content
Go to file

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!)

Try it out

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


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: '', 
  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' }

Try this example in a Fiddle

API documentation


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.

Running a schema


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 }

checkAsync, checkByKeyAsync and resultAsync

The async versions of check, checkByKey and result respectively.

Combinator API


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 = 

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'), 


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

Basic schemas


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 = 
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 = 
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 = 

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 =

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.

Collection related schemas



Create a schema on arrays from a schema on its values.

array :: <A, B>(v : Schema<A, B>,) : Schema_<A[], B[]>
const schema =

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 =

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 = 

check(new Set([1, 'a', true], schema, enUS)
// null

check([1, 'a', true], schema, enUS)
// 'Must be a set'
const schema = 

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

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


Schema<A, B>

The type of a schema. Converts a value of type A into one of type B and validates the result.

Schema_<A, B>

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)

You can’t perform that action at this time.