Composable schema functions for input transformation and validation
yarn add schema-fns
# or
npm install schema-fns
schema-fns
lets you build composable schemas for input transformation and
validation. The most important export is the schema()
function that's used to
define schemas. It's modeled after the Express middleware model of layers of
handler functions and therefore accepts as parameters any number of functions or
schema objects. Those functions have a signature of (value, update, error)
,
where:
value
is the input value to be transformed and validated,update
is a function used to return a modified input value, anderror
is a function used to create validation errors.
Let's see it in action. The first thing you can do with schema-fns
is
transform input. The following example uses two transformation functions, one
to convert the value to a string, and another to append a "1"
. Given the input
42
, the value becomes "421"
:
const { schema } = require("schema-fns");
const MySchema = schema(
function (value, update, error) {
update(String(value));
},
function (value, update, error) {
update(value + "1");
},
);
const { value } = MySchema.validate(42);
console.log(value); //=> "421"
For simple value tranformations, there's an adapter function mapAdapter()
that
converts mapping functions to a form that works for schema-fns
. The same
example above can be written as:
const { schema, mapAdapter } = require("schema-fns");
const MySchema = schema(
mapAdapter(value => String(value)),
mapAdapter(value => value + "1"),
);
const { value } = MySchema.validate(42);
console.log(value); //=> "421"
The next interesting thing you can do is validate input values. And the simplest
way is to use built-in validation functions. Calling .validate(input)
returns
an object with:
valid
:true
if there were no validation errors,false
otherwisevalue
: the transformed valueerrors
: an array of any validation errors
Here's an example that validates that a value is an object.
const { schema, is } = require("schema-fns");
const MySchema = schema(
is(Object),
);
const { valid, value, errors } = MySchema.validate(42);
console.log(valid); //=> false
console.log(value); //=> 42 (because the value was never updated)
The errors
array will look like the following:
[{
code: "is.type", // the validation error code
path: [], // path taken to get from the input to the value where the error happened
message: "wrong type: expected object", // the default
value: 42, // the value validated
expectedType: "object", // each validator may include additional context
}]
In addition to .validate()
, there is a .test()
function that just returns
the value of valid
:
const { schema, is } = require("schema-fns");
const MySchema = schema(
is(Object),
);
console.log(MySchema.test(42)); //=> false
console.log(MySchema.test({})); //=> true
console.log(MySchema.test("hi")); //=> false
Finally, if instead of receiving the transformed value and errors, .assert()
will throw an error if validation fails:
const { schema, is } = require("schema-fns");
const MySchema = schema(
is(Object),
);
MySchema.assert(42); // throws
MySchema.assert("hi"); // throws
MySchema.assert({}); // doesn't throw
Let's look at more validation functions. hasKeys()
checks that keys are
present on some object:
const { schema, hasKeys } = require("schema-fns");
const MySchema = schema(
hasKeys("foo", "bar"),
);
const { valid, value, errors } = MySchema.validate({});
console.log(valid); //=> false
console.log(value); //=> {}
The errors
array will look like:
[{
code: "key.missing",
path: [],
message: `expected key "foo" missing`,
key: "foo",
value: {},
}, {
code: "key.missing",
path: [],
message: `expected key "bar" missing`,
key: "bar",
value: {},
}]
The key()
function is used to focus in on one key of an object and apply
schema functions to it. The first argument is the key name and the remaining
arguments are schema functions.
const { schema, key, mapAdapter } = require("schema-fns");
const MySchema = schema(
key(
"name",
mapAdapter(value => `Hello, ${value}!`),
mapAdapter(value => String(value).toUpperCase()),
),
);
const { valid, value, errors } = MySchema.validate({ name: "World" });
console.log(valid); //=> true
console.log(value); //=> { "name": "HELLO, WORLD!" }
console.log(errors); //=> []
It can also be used for validation:
const { schema, is, key } = require("schema-fns");
const MySchema = schema(
key("foo", is(Object)),
);
const { valid, value, errors } = MySchema.validate({ foo: 42 });
console.log(valid); //=> false
console.log(value); //=> { foo: 42 }
The errors
array will look like:
[{
code: "is.type",
path: ["foo"],
message: "wrong type: expected object",
expectedType: "object",
value: 42,
}]
Of course, key()
is recursive:
const { schema, is, key } = require("schema-fns");
const MySchema = schema(
key(
"name",
is(Object),
key("first", is(String)),
),
);
const { valid, value, errors } = MySchema.validate({
name: {
first: 42,
},
});
console.log(valid); //=> false
console.log(value); //=> { name: { first: 42 } }
The errors
array will look like:
[{
code: "is.type",
path: ["name", "first"],
message: "wrong type: expected string",
expectedType: "string",
value: 42,
}]
The items()
function is used to transform and validate all items in an array.
Here's a transformation example that doubles numbers in an array and then
repeats the digits:
const { schema, items, mapAdapter } = require("schema-fns");
const MySchema = schema(
items(
mapAdapter(value => value * 2),
mapAdapter(value => String(value) + String(value)),
mapAdapter(Number)
),
);
const { valid, value, errors } = MySchema.validate([ 1, 2, 3 ]);
console.log(valid); //=> true
console.log(value); //=> [ 22, 44, 66 ]);
It can be used for validation as well:
const { schema, is, items } = require("schema-fns");
const MySchema = schema(
items(is(Number)),
);
const { valid, value, errors } = MySchema.validate([ 1, 2, "3", 4, "5" ]);
console.log(valid); //=> false
console.log(value); //=> [ 1, 2, "3", 4, "5" ]
The errors
array will look like:
[{
code: "is.type",
path: [2],
message: "wrong type: expected number",
expectedType: "number",
value: "3",
}, {
code: "is.type",
path: [4],
message: "wrong type: expected number",
expectedType: "number",
value: "5",
}]
Writing custom validators is as simple as calling the error()
function,
passing a string error code and a context object with additional information
useful for debugging or messaging to an end user.
const { schema, is, items } = require("schema-fns");
const MySchema = schema(
notFoo(),
);
// fails validation if the value equals "foo", "FOO", etc
function notFoo() {
return function (value, update, error) {
if (String(value).toLowerCase() === "foo") {
// first argument is a unique error code
// second argument is a context object
error("not.foo", {
message: `not foo expected, got ${value}`,
value,
});
}
};
}
console.log(MySchema.test("foo")); //=> false
console.log(MySchema.test("fOO")); //=> false
console.log(MySchema.test("FOo")); //=> false
console.log(MySchema.test(42)); //=> true
console.log(MySchema.test({})); //=> true
Inspired by Joi and superstruct.