The package has been developed with reference to: AJV JSON Schema
AJV
is a popular open-source library for validating data against JSON Schema. It is widely used in JavaScript and TypeScript
projects to ensure data integrity, particularly in API request validation, configuration files, and structured data processing.
Although the AJV
library offers TypeScript
support, it doesn’t provide built-in tools for defining schemas in TypeScript
. To bridge this gap, I’ve published an npm package called @raminyavari/ajv-ts-schema
. This package simplifies schema definition while maintaining TypeScript
compatibility.
Here’s how it works. Instead of:
const schema = {
type: "object",
properties: {
foo: {
type: "number",
multipleOf: 5,
minimum: 0,
maximum: 100,
},
bar: {
type: "string",
nullable: true,
maxLength: 20,
}
},
required: ["foo"]
};
You can do this:
@AjvObject()
class MySchema extends AjvSchema {
@AjvProperty({
type: "number",
multipleOf: 5,
minimum: 0,
maximum: 100,
required: true,
})
foo!: number;
@AjvProperty({ type: "string", nullable: true, maxLength: 20 })
bar?: string | null;
}
const schema = mySchema.getSchema(); // This gives you the AJV JSON schema
The key advantage of the second approach over the first is that it is fully typed, offering several significant benefits:
To get started, install the package by running one of the following commands:
npm install @raminyavari/ajv-ts-schema
Or, if you’re using Yarn:
yarn add @raminyavari/ajv-ts-schema
This package uses experimental decorators and includes reflect-metadata
as a dependency. To use it correctly, you must enable experimentalDecorators
and emitDecoratorMetadata
in your tsconfig.json
. Here's how to configure it:
{
"compilerOptions": {
"target": "ES6", // or higher
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
If you want to use this package along with ajv
, it is recommended to also install ajv-formats
and use ajv draft 2020
. Here is how to use along with ajv
:
import Ajv from "ajv/dist/2020";
import addFormats from "ajv-formats";
import { AjvSchema, AjvObject, AjvProperty, getSchema } from "@raminyavari/ajv-ts-schema";
const ajv = new Ajv({ useDefaults: true });
addFormats(ajv);
@AvjObject({ /* ...options */ })
class MySchema extends AjvSchema {
@AjvProperty({ type: "string" /*, ...string options */ })
foo?: string;
// ...other properties
}
const myController = (input: any) => {
const schema = MySchema.getSchema();
const validate = ajv.compile(schema);
const isValid = validate(input);
if (!isValid) {
const errors = validate.errors;
return { }; // BadRequest with the list of errors
}
const typedInput = AjvSchema.fromJson(MySchema, input); // returns an instance of MySchema
// the rest of the code
};
The package provides the following key exports:
AjvSchema
: A base class for defining all your schemas. It includes two static methods, getSchema
and fromJson
, which are explained later.
AjvObject
: A class decorator used to mark a class as a schema.
AjvProperty
: A property decorator for defining schema properties.
getSchema
: A utility function for converting property schemas into an AJV
schema. While the expected input is typically a JSON object, you can also use getSchema
for primitives and arrays.
An AJV
schema is a JSON object that defines a type
along with its associated options. It typically looks like this:
{
type: "object" | "string" | "number"| "integer" | "boolean" | "array",
...options
}
Each type
in AJV
schemas has a unique set of options. For example, the object
type refers to a JSON object, meaning the input must conform to a JSON structure. Here's an example:
schema: { type: "object", properties: { foo: { type: "string" } } }
input: { foo: "abc" }
With @raminyavari/ajv-ts-schema
, the object
type is represented as a class that extends AjvSchema
and is decorated with @AjvObject
. Each property of the object is represented by a class property, decorated with @AjvProperty
.
This transforms the schema above into the following TypeScript
representation:
@AjvObject()
class MySchema extends AjvSchema {
@AjvProperty({ type: "string" })
foo?: string;
}
You can retrieve the schema using MySchema.getSchema()
.
However, there are cases where the input is a primitive type or an array. Since these are not objects, they cannot be represented using a class. For example:
schema: { type: "string", minLength: 2 }
input: "abc"
In such scenarios, you can use the getSchema
function. Here's how:
const schema = getSchema({ type: "string", minLength: 2 });
You might notice that in this example, the input and output of getSchema
appear to be identical. However, this is not always the case. The key advantage of using getSchema
is that it is fully typed, ensuring type safety and preventing errors during development.
The AjvSchema
class provides a static method, fromJson
, which converts a validated JSON object into your defined schema.
Here’s an example:
@AjvObject({ ...options })
class MySchema extends AjvSchema {
@AjvProperty({ type: "formatted-string", format: "email" })
email?: string;
}
const myController = (input: any) => {
const schema = MySchema.getSchema();
const validate = ajv.compile(schema);
const isValid = validate(input);
if (!isValid) return; // return a bad request with error messages
// Serialize the 'input' to an instance of 'MySchema'
const typedInput = AjvSchema.fromJson(MySchema, input);
}
The options are based on those outlined in the AJV
documentation: https://github.com/ajv-validator/ajv/blob/master/docs/json-schema.md. However, there are some important differences to note.
Below is a list of key differences and considerations:
While not all options and customizations are supported, the first version of the package provides comprehensive coverage, addressing nearly all common use cases—except for very specific edge cases.
In AJV
schemas, required
is a property of the schema when the type is object
.
const schema = {
type: "object",
properties: {
foo: { type: "string" },
bar: { type: "integer" }
},
required: ["foo"]
}
In the package, required
is an option applied to the property within the schema.
@AjvObject()
class MySchema extends AjvSchema {
@AjvProperty({ type: "string", required: true })
foo!: string;
@AjvProperty({ type: "integer" })
bar?: number;
}
In AJV
schemas, a string can have either a pattern
or a format
option, but not both simultaneously.
const stringWithPattern = { type: "string", pattern: "^x.*$" };
const stringWithFormat = { type: "string", format: "email" };
In the package, a string with a format
is transformed into a property of type formatted-string
. Here’s an example:
@AvjProperty({ type: "string", pattern: "^x.*$" })
foo!: string;
@AvjProperty({ type: "formatted-string", format: "email" })
bar!: string;
Schemas of type object
in AJV
can use the dependentRequired
option, available in Draft 2020. This option specifies that the requirement of certain properties depends on the presence of other properties. Here’s an example:
@AjvObject<MySchema>({ dependentRequired: { foo: ["bar", "baz"] } })
class MySchema extends AjvSchema {
@AjvProperty({ type: "integer" })
foo?: number;
@AjvProperty({ type: "integer" })
bar?: number;
@AjvProperty({ type: "integer" })
baz?: number;
}
The first thing to note is that the class is passed as a generic type parameter (@AjvObject
). This allows dependentRequired
to leverage the type parameter for type safety, ensuring that only valid dependencies can be specified—random values cannot be added as dependencies.
Additionally, none of these properties are inherently required. When foo
is marked as dependent on bar
and baz
, it means that if foo
is provided, the other two must also be included. If either bar
or baz
is missing, the input will be considered invalid.
A property can be an object, in which case it must first be defined as a schema.
As an example, if the desired AJV
schema is:
{
type: "object",
properties: {
id: { type: "string" }
category: {
type: "object",
properties: {
values: {
type: "array",
item: {
type: "object",
properties: {
id: { type: "integer" },
title: { type: "string" }
}
}
}
}
}
}
}
It should be defined as follows:
@AjvObject()
class ValueSchema extends AjvSchema {
@AjvProperty({ type: "integer" })
id?: number;
@AjvProperty({ type: "string" })
title?: string;
}
@AjvObject()
class CategorySchema extends AjvSchema {
@AjvProperty({ type: "array", items: ValueSchema })
values?: ValueSchema[];
}
@AjvObject()
class MySchema extends AjvSchema {
@AjvProperty({ type: "string" })
id?: string;
@AjvProperty(CategorySchema)
category?: CategorySchema;
}
You can then obtain the JSON representation of the schema by calling MySchema.getSchema()
.
For more details about the options below, refer to the following documentation or check the tooltips provided by your code editor’s auto-completion feature.
General Options
Below is a list of options available for all types when using @AjvObject
and @AjvProperty
:
required
, nullable
, enum
, const
, default
, not
, oneOf
, anyOf
, and allOf
.
string
minLength
, maxLength
, and pattern
.
formatted-string
minLength
, maxLength
, and format
.
number & integer
minimum
, maximum
, exclusiveMinimum
, exclusiveMaximum
, and multipleOf
.
array
minItems
, maxItems
, uniqueItems
, prefixItems
, items
, contains
, minContains
, and maxContains
.
object
minProperties
, maxProperties
, patternProperties
, additionalProperties
, and dependentProperties
.