Suretype is a JSON validator targeting TypeScript and JSON Schema. It is ridiculously type safe when used in TypeScript, which is good for accuraccy, but also for aiding IDE auto-complete.
It's as easy as Joi, but ~50x faster.
It's (at least) as typesafe as Superstruct, but ~80x faster. ~3000x faster than Zod and ~220x faster than ow.
These are x (times) not %
Benchmark results.
❯ yarn benchmark
Joi x 384,285 ops/sec ±0.42% (94 runs sampled)
Superstruct x 248,358 ops/sec ±0.54% (93 runs sampled)
Zod x 7,264 ops/sec ±1.06% (87 runs sampled)
ow x 94,136 ops/sec ±0.78% (90 runs sampled)
SureType x 21,371,281 ops/sec ±0.86% (90 runs sampled)
-----
56x faster than Joi
86x faster than Superstruct
2942x faster than Zod
227x faster than ow
It supports most (if not all) of JSON schema, and nothing beyond that, so that the validator schemas written in TypeScript (or JavaScript) can be ensured to be convertible into JSON schema. This also prevents suretype from becoming feature bloated - it has a small and extremely simple API.
Errors are prettified using awesome-ajv-errors.
From a validator schema defined with suretype, you can trivially:
- Compile a validator function (using the very fast Ajv)
- Extract the corresponding JSON Schema
- Deduce a TypeScript type corresponding to the validator schema (at compile-time!)
Minimal example
The following is a validator schema using suretype:
import { v } from "suretype"
const userSchema = v.object( {
firstName: v.string( ).required( ),
lastName: v.string( ),
age: v.number( ).gte( 21 ),
} );
This schema object can be compiled into validator functions, and it can be used to deduce the corresponding TypeScript type:
import type { TypeOf } from "suretype"
type User = TypeOf< typeof userSchema >;
This type is compile-time constructed (or deduced), and is semantically identical to:
interface User {
firstName: string;
lastName?: string;
age?: number;
}
Note the ?
for the optional properties, i.e. those that aren't followed by required()
.
There are three ways of compiling a validator function; choose the one that best fits your application and situation. Given:
import { compile } from "suretype"
const data = ...
Standard validator
The default behaviour of compile
is to return a validator function returning extended Ajv output.
const userValidator = compile( userSchema );
userValidator( data );
The explanation
is a pretty-printed error.
Type-guarded validator
Use the second optional argument to specify simple
mode. The return is a boolean, type guarded.
const isUser = compile( userSchema, { simple: true } );
isUser( data );
if ( isUser( data ) ) {
data.firstName;
} else {
data.firstName;
}
Type-ensured validator
Specify ensure
mode to get a validator function which returns the exact same output as the input (referentially equal), but with a deduced type. This is often the most practical mode.
const ensureUser = compile( userSchema, { ensure: true } );
ensureUser( data );
const user = ensureUser( data );
user.firstName;
user.foo;
On validation failure, the error thrown will be of the class ValidationError
, which has both the raw Ajv errors as an errors
property, and the pretty explanation in the property explanation
.
Note: The returned ensurer function can optionally take a type parameter as long as it is equal to or compatible with the deduced type. This means that if the type is exported from suretype to decorated TypeScript declaration files (with annotations), those types can be used as a type parameter, and the returned type will be that type. Example:
import type { User } from './generated/user'
const user = ensureUser< User >( data );
Raw JSON Schema validator
Sometimes it's handy to not describe the validator schema programmatically, but rather use a raw JSON Schema. There will be no type deduction, so the corresponding interface must be provided explicitly. Only use this if you know the JSON Schema maps to the interface! raw
works just like the v.*
functions and returns a validator schema. It can also be annotated.
import { raw, compile } from 'suretype'
type User = ...;
const userSchema = raw< User >( { type: 'object', properties: { } } );
const ensureUser = compile( userSchema, { ensure: true } );
Annotating schemas
You can annotate a validator schema using suretype()
or annotate()
. The return value is still a validator schema, but when exporting it, the annotations will be included.
The difference between suretype()
and annotate()
is that suretype()
requires the name
property, where as it's optional in annotate()
. Use suretype()
to annotate top-level schemas so that they have proper names in the corresponding JSON Schema.
Annotations are useful when exporting the schema to other formats (e.g. JSON Schema or pretty TypeScript interfaces).
import { suretype, annotate, v } from "suretype"
const cartItemSchema = suretype(
{ name: "CartItem" },
v.object( {
productId: annotate( { title: "The product id string" }, v.string( ) ),
} )
);
The interface (i.e. the fields you can use) is called Annotations
:
interface Annotations {
name: string;
title?: string;
description?: string;
examples?: Array< string >;
}
where only the name
is required.
Thorough example
The following are two types, one using (or depending on) the other. They are named, which will be reflected in the JSON schema, shown below.
The userSchema
is the same as in the above example, although it's wrapped in suretype()
which annotates it with a name and other attributes.
Given these validation schemas:
import { suretype, v } from "suretype"
const userSchema = suretype(
{
name: "V1User",
title: "User type, version 1",
description: `
A User object must have a firstName property,
all other properties are optional.
`,
examples: [
{
firstName: "John",
lastName: "Doe",
}
],
},
v.object( {
firstName: v.string( ).required( ),
lastName: v.string( ),
age: v.number( ).gte( 21 ),
} )
);
const messageSchema = suretype(
{
name: "V1Message",
title: "A message from a certain user",
},
v.object( {
user: userSchema.required( ),
line: v.string( ).required( ),
} )
);
The JSON schema for these can be extracted, either each type by itself:
import { extractSingleJsonSchema } from "suretype"
const { schema: jsonSchema } = extractSingleJsonSchema( userSchema );
or as all types at once, into one big JSON schema. In this case, all validation schemas provided must be wrapped with suretype()
, as they will become JSON schema "definitions" and therefore must have at least a name.
import { extractJsonSchema } from "suretype"
const { schema: jsonSchema } = extractJsonSchema( [ userSchema, messageSchema ] );
The jsonSchema
object (which can be JSON.stringify
'd) will be something like:
JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"V1User": {
"title": "User type, version 1",
"description": "A User object must have a firstName property,\nall other properties are optional.",
"examples": [
{
"firstName": "John",
"lastName": "Doe"
}
],
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"age": { "type": "number", "minimum": 13 }
},
"required": [ "firstName" ]
},
"V1Message": {
"title": "A message from a certain user",
"type": "object",
"properties": {
"user": { "$ref": "#/definitions/V1User" },
"line": { "type": "string" }
},
"required": [ "user", "line" ]
}
}
}