Schemaglobin: Validate unknown user input against schemas
Schemaglobin is a schema validator for user input written for JavaScript and TypeScript with special attention paid to TypeScript types.
Installation
npm install schemaglobin
Usage
Import schema creator functions (e.g. string()
). These functions take an options
object that configures the schema and returns an instance of the corresponding schema class.
When you have created a schema you can pass unknown values into the validate()
method to validate those values:
- Valid values will be returned unchanged.
- Invalid values that can be trivially fixed will be modified and returned.
- Invalid values will return instances of
Invalid
, which contains a user-facing message
property describing why.
This basic example shows how Schemaglobin can be used in the real world:
import { object, string, number, boolean, Invalid } from "schemaglobin";
const schema = object(
required: true;
props: {
"name": string({ required: true }),
"age": number({ required: true, min: 0, max: 150 }),
"status": boolean(),
}
);
export function myServerFunction(unsafeInput: unknown): true {
const data = schema.validate(unsafeInput);
if (data instanceof Invalid) throw new ServerError("Invalid input: " + data.message);
const status = saveToDatabase(data);
return true;
}
Invalid values
Is you pass an invalid value into validate()
then two things might happen: 1) If the invalid value can be trivially converted to a valid value without data loss, it will be converted and returned, or 2) An instance of Invalid
will be returned:
import { string, number, email, url, boolean, Invalid } from "schemaglobin";
boolean().validate("abc");
boolean().validate("");
string().validate(123);
number().validate("123.456");
string().validate(true);
number().validate("abc");
email().validate("abc");
url().validate("abc");
With object schemas, options.props
is used to fill (and trivially convert) missing object props:
import { object, number, string } from "schemaglobin";
const schema = object({
props: {
name: string({ value: "DEFAULT" }),
age: number(),
},
});
schema.validate({ age: "123" });
schema.validate({ name: "Dave" });
Instances of Invalid
contain a string .message
property describing the issue:
const invalid = url().validate("abc");
console.error(invalid.message);
When validating an object, it's possible the contents might be invalid. In this situation Invalid
also has a .messages
object specifying where, within the object, the data was invalid.
import { object, string, number } from "schemaglobin"
const schema = object({
props: {
name: string({ required: true }),
age: number({ min 0, max: 200 }),
}
});
const invalid = schema.validate({ age: 900 });
console.log(invalid.message);
console.log(invalid.messages);
This also works for arrays. The keys in .messages
will be numeric strings:
import { array, string } from "schemaglobin";
const schema = array({
items: string({ required: true }),
});
const invalid = schema.validate([123, true, ""]);
console.log(invalid.message);
console.log(invalid.messages);
Validating different types
Schemaglobin contains a bunch of different schema types you can use:
import { boolean, string, number, date, distance, email, phone, url, key, array, object, map } from "schemaglobin";
const booleanSchema = boolean({ required: true, ...etc });
const stringSchema = string({ required: true, ...etc });
const numberSchema = number({ required: true, ...etc });
const colorSchema = color({ required: true, ...etc });
const dateSchema = date({ required: true, ...etc });
const distanceSchema = distance({ required: true, unit: "foot", ...etc });
const emailSchema = email({ required: true, ...etc });
const phoneSchema = phone({ required: true, ...etc });
const urlSchema = url({ required: true, ...etc });
const keySchema = key({ required: true, ...etc });
const arraySchema = array({ required: true, items: etc, ...etc });
const objectSchema = object({ required: true, props: etc, ...etc });
const mapSchema = object({ required: true, items: etc, ...etc });
booleanSchema.validate(true);
stringSchema.validate("abc");
numberSchema.validate(12345);
colorSchema.validate("#00CCFF");
dateSchema.validate("1995");
distanceSchema.validate("100 yd");
emailSchema.validate("me@x.com");
phoneSchema.validate("+1234567890");
urlSchema.validate("http://x.com");
keySchema.validate("ajdk29Jak");
arraySchema.validate(["a", 2, true]);
objectSchema.validate({ a: "A" });
mapSchema.validate({ a: "A" });
stringSchema.validate(true);
numberSchema.validate(true);
dateSchema.validate("aaaaaaa");
distanceSchema.validate("aaaaaaa");
colorSchema.validate(true);
emailSchema.validate("111111");
phoneSchema.validate("aaaaaa");
urlSchema.validate("11111111");
keySchema.validate("!!!!!!!");
arraySchema.validate(true);
objectSchema.validate(true);
mapSchema.validate(true);
Default values
Every schema has a default value that is used when the value is undefined
. The default value can be changed for any schema with options.value
import { string } from "schemaglobin";
const schemaWithoutDefault = string();
schemaWithDefault.validate();
const schemaWithDefault = string({ value: "WOW VALUE" });
schemaWithDefault.validate();
schemaWithDefault.validate(undefined);
Required values
Normally values are not required, meaning null
or ""
empty string are allowed. This can be changed with options.required
import { number } from "schemaglobin";
const optionalSchema = number({ required: false });
optionalSchema.validate(null);
const requiredSchema = number({ required: true });
optionalSchema.validate(null);
Using TypeScript
Schemaglobin pays special attention to the TypeScript type of values returned by validate()
, for example:
NumberSchema.validate()
- Normally returns
number | null | Invalid
- If
options.required
is truthy the value will never be null
(as that would be invalid) so it only returns number | Invalid
StringSchema.validate()
- Normally returns
string | Invalid
- If
options.options
is set it can return a more specific type, e.g. "a" | "b" | "" | Invalid
import { object, string, number, boolean, Invalid } from "schemaglobin";
const requiredNumber: number | Invalid = number({ required: true }).validate(123);
const optionalNumber: number | Invalid = number({ required: false }).validate(123);
const enumStringArray: "a" | "b" | Invalid = string({ options: ["a", "b"] }).validate("a");
const enumStringObject: "a" | "b" | Invalid = string({ options: { a: "A", b: "B" } }).validate("a");
const objectSchema = object({
required: true,
props: {
num: number(),
str: string({ required: true }),
bool: boolean({ required: true }),
},
});
const obj = objectSchema.validate(undefined);
if (!(obj instanceof Invalid)) {
const num: number | null = obj.num;
const str: string = obj.str;
const bool: true = obj.bool;
}
Schemaglobin also provides the SchemaType<Schema>
helper type, which allow you to extract the type of a schema:
import { string, SchemaType } from "schemaglobin";
const requiredStringSchema = string({ required: true });
const optionalStringSchema = string({ required: false });
const requiredEnumSchema = string({ required: true, options: ["a", "b"] });
const optionalEnumSchema = string({ required: false, options: ["a", "b"] });
type RequiredStringType = SchemaType<typeof requiredStringSchema>;
type OptionalStringType = SchemaType<typeof optionalStringSchema>;
type RequiredEnumType = SchemaType<typeof requiredEnumSchema>;
type OptionalEnumType = SchemaType<typeof optionalEnumSchema>;
const objectSchema = object({ props: { str: string(), num: number() } });
type ObjectType = Schematype<typeof objectSchema>;
Reference
All schema creator functions allow the following options (and may allow others too):
options.title: string = ""
- A title for the schema (for showing in a user-facing field).options.description: string = ""
- A description for the schema (for showing in a user-facing field).options.placeholder: string = ""
- A placeholder for the schema (for showing in a user-facing field).
array()
The array()
creator function creates a ArraySchema
instance that can validate arrays and their contents:
- Arrays are valid, e.g.
[1,2,3]
- Contents of the array can be validated with
options.items
- Falsy values are converted to
[]
empty array. []
empty array is an invalid value if options.required
is truthy.
array()
also allows the following options:
options.value: [] = []
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then empty arrays will return Invalid("Required")
options.min: number = null
- The minimum number of items allowed.options.max: number = null
- The maximum number of items allowed.options.items: Schema
(required) - Schema that will be used to validate the contents of the array.
boolean()
The boolean()
creator function creates a BooleanSchema
instance:
- All truthy values are converted to
true
- All falsy values are converted to
false
- Default value is
false
false
is an invalid value if options.required
is truthy.- Doesn't accept any additional options.
boolean()
also allows the following options:
options.value: boolean = false
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then false values will return Invalid("Required")
color()
The color()
creator function creates a ColorSchema
instance that can validate hexadecimal color strings:
- Strings in hex color format are valid, e.g.
#00CCFF
- Whitespace is trimmed automatically.
- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
color()
also allows the following options:
options.value: Date = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
date()
The date()
creator function creates a DateSchema
instance that can validate date YMD strings:
- Strings in YMD format are valid, e.g.
1995-10-20
- Whitespace is trimmed automatically.
- Strings in other formats are parsed with
new Date()
and converted to YMD strings. Date
instances and numbers are converted to YMD strings.value
, min
and max
options can be functions that return calculated values, e.g. using value: Date.now
will set the value to today's date.- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
date()
also allows the following options:
options.value: Date = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
options.min: string = null
- The minimum date allowed.options.max: string = null
- The maximum date allowed.
distance()
The distance()
creator function creates a DistanceSchema
instance that can validate distance numbers:
- Numbers are valid values (and are assumed to be the base unit).
- Numeric strings are valid values and are converted to numbers.
- Numeric strings with unit suffixes (e.g.
10km
or 99 inches
) are valid values and are converted to a number and converted to the base unit. 0
zero is a valid value.- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
distance()
also allows the following options:
options.value: number | null = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
options.min: number = null
- The minimum number allowed.options.max: number = null
- The maximum number allowed.options.step: number = null
- The step size for the the number (the value will be rounded to the closest step).options.unit: DistanceUnit = "meter"
- The base unit for this schema.
email()
The email()
creator function creates a EmailSchema
instance that can validate email addresses:
- Strings that are valid email addresses are valid, e.g.
dave@gmail.com
- Whitespace is trimmed automatically.
- Email is converted to lowercase automatically.
- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
email()
also allows the following options:
options.value: string = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
key()
The key()
creator function creates a KeySchema
instance that can validate database key strings:
- Strings that are valid database keys are valid, e.g.
abc
or AAAA1234
- By default key strings can only contain
a-zA-Z0-9
or -
hyphen. - Whitespace is trimmed automatically.
- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
key()
also allows the following options:
options.value: string = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
options.match: RegExp = /[a-zA-Z0-9-]{1,24}/
- Format the database key must match.
map()
The map()
creator function creates a MapSchema
instance that can validate an object containing a list of key: value entries:
- Objects are valid, e.g.
{ a: 1, b: 2, c: 3 }
- Contents of the object can be validated with
options.props
- Falsy values are converted to
{}
empty object. {}
empty object is an invalid value if options.required
is truthy.
map()
also allows the following options:
options.value: {} = {}
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then empty objects will return Invalid("Required")
options.min: number = null
- The minimum number of items allowed.options.max: number = null
- The maximum number of items allowed.options.items: Schema
(required) - Schema that will be used to validate all properties in the object.
number()
The number()
creator function creates a NumberSchema
instance that can validate numbers:
- Numbers are valid values.
- Strings that can be converted to numbers are valid values.
0
zero is a valid value.- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
number()
also allows the following options:
options.value: number | null = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
options.min: number = null
- The minimum number allowed.options.max: number = null
- The maximum number allowed.options.options: number[] | { number: string } = null
- Explicit list of allowed values as either:
- An array of numbers where each number is an allowed value.
- An object where each number key is an allowed value and the corresponding value can be a user-facing title for the option.
options.step: number = null
- The step size for the the number (the value will be rounded to the closest step).
phone()
The phone()
creator function creates a PhoneSchema
instance that can validate URLs:
- Strings that are valid phone numbers are valid, e.g.
+441234567890
- Whitespace is trimmed automatically.
- Non-digit characters are stripped automatically.
- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
phone()
also allows the following options:
options.value: string = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
object()
The object()
creator function creates a ObjectSchema
instance that can validate an exact object:
- Objects are valid, e.g.
{ a: 1, b: "two" }
- Contents of the object can be validated with
options.props
- Properties not specified in `options.props
- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy.
object()
also allows the following options:
options.value: {} | null = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
options.props: { [prop: string]: Schema }
(required) - An object explicitly specifying the type of each individual property.
string()
The string()
creator function creates a StringSchema
instance:
- Strings are valid values.
- Whitespace is trimmed automatically.
- Control characters are stripped automatically.
- Newlines (and tabs) are stripped unless the
multiline
option is true
- Numbers are converted to string automatically.
- Falsy values are converted to
""
empty string - Default value is
""
empty string ""
empty string is an invalid value if options.required
is truthy.
string()
also allows the following options:
options.value: string = ""
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then empty strings will return Invalid("Required")
options.min: number = 0
- The minimum number of characters allowed.options.max: number = null
- The maximum number of characters allowed.options.options?: string[] | { string: string }
- Explicit list of allowed values as either:
- An array of strings where each string is an allowed value.
- An object where each string key is an allowed value, and the corresponding value is a user-facing title for the option.
options.match: RegExp = null
- A regular expression that the string must match.options.multiline: boolean = false
- Whether the string allows newlines or not
url()
The url()
creator function creates a UrlSchema
instance that can validate URLs:
- Strings that are valid email addresses are valid, e.g.
https://x.com
or data:anything
- Whitespace is trimmed automatically.
- Falsy values are converted to
null
null
is an invalid value if options.required
is truthy
url()
also allows the following options:
options.value: string = null
- The default value which will be used if the value is undefined
options.required: boolean = false
- If true, then null values will return Invalid("Required")
options.schemes: string[] = ["http:", "https:"]
- Whitelist of allowed URL schemes.options.hosts: string[] = null
- List of allowed hostnames, e.g. ["google.com"]
options.max: number = 512
- Maximum length of a URL.
Shortcuts
The following static values are available as shortcuts attached to the creator functions for all simple values:
import { boolean, date, distance, email, key, number, phone, string, url } from "schemaglobin";
boolean.required.validate(true);
boolean.optional.validate(false);
color.required.validate("#00CCFF");
color.optional.validate(null);
date.required.validate(new Date());
date.optional.validate(null);
distance.required.validate("123 km");
distance.optional.validate(null);
email.required.validate("dave@x.com");
email.optional.validate(null);
key.required.validate("abc123");
key.optional.validate(null);
number.required.validate(12345);
number.optional.validate(null);
number.timestamp.validate(Date.now());
phone.required.validate("+44123456789");
phone.optional.validate(null);
string.required.validate("AAAAA");
string.optional.validate("");
url.required.validate("https://x.com");
url.optional.validate(null);
Object/map/array schemas also provide shortcuts, but as options.props
and options.items
are required these must be passed in as the only argument:
import { object, array, map, number, string, boolean } from "schemaglobin";
object.required({ num: number.optional }).validate({ num: 123 });
object.optional({ num: number.required }).validate(null);
array.required(string.required).validate([1, 2, 3]);
array.optional(string.required).validate([]);
map.required(boolean.required).validate({ a: 1, b: 2, c: 3 });
map.optional(boolean.required).validate(null);
Changelog
See Releases