TypeScript validation with JSON schemas
By: Alex Pickering
Tags: Validation, Schema, Typescript, Ajv, Json

Someone Say AJV?

Let’s create a simple service that has an injected instance of AJV with a function to validate some user data.

Instantiating AJV is relatively straight forward. The constructor accepts an optional configuration, which for the purpose of this short guide, we shall omit.

import * as Ajv from 'ajv' const ajv = new Ajv()

If you’re wanting to inject this into some custom validator class that you’re creating, you can use the Ajv interface to type the property in the constructor.

import { Ajv } from 'ajv' class Foo {

constructor(protected ajv: Ajv) {

}

}

If we were to then import the schema that we are wanting to use, we can very simply import the JSON object. We’ll be exploring a simple JSON schema later in this post. Ensure that in your tsconfig.json you have the following property enabled first "resolveJsonModule": true .

import * as requestSchema from '../foo/bar/requestSchema.json'

Now we’re ready to create a basic function that will take in the user-submitted data and the schema as parameters, and validate the data against the given schema.

public validate(schema, data) {

return this.ajv.validate(schema, data)

}

The validate() function exposed on AJV will return a boolean indicating whether or not the data is valid according to the given schema. If the data failed the validation, you can call the exposed function errorsText() to get the relevant messages for the errors that occurred whilst validating the data.

Let’s look at a basic implementation of how we can handle the failed validation when it occurs. As you can see, the errors are available on the ajv object we injected.

public validate(schema, data) {

const isValid = this.ajv.validate(schema, data) if (!isValid) {

const errorMessages = this.ajv.errorsText()

throw new Error(`Validation Error. ${errorMessages}`)

} return data

}

It’s important to note that the exposed function errorsText() returns a string and the errors are comma deliminated. Personally, I would create a custom named error that accepts the error messages as a parameter and throw the named error here instead of a generic error, but that’s just me.

Note: You can also access the errors property on the ajv object which is an array of error objects which contain some more information about the failed validation. You can experiment with this if you’d prefer.

Slightly More Advanced

Let’s assume that we may need to reuse this function multiple times and we want to have a specific type returned, how could we accomplish this easily?

One word: Generics!

public validate<T>(schema: object, data: T): T {

const isValid = this.ajv.validate(schema, data)



if (!isValid) {

const errorMessages = this.ajv.errorsText()

throw new Error(`Validation Error. ${errorMessages}`)

}



return data

}

We can simply allow the type of T to be used as a type definition here. The function will return the same data that was inputted or throw an error (in which case the return type is considered to be never ).

It becomes dead easy to call and utilize this function in the end. Calling the function where SOME_TYPE is the type definition of the object we are expecting the user data to conform to.

import * as schema from '../some/schema.json'

import * as userData from '../some/user/data.json'



const ajv = new Ajv()

const foo = new Foo(ajv)



const validData: SOME_TYPE = foo.validate<SOME_TYPE>(schema, userData)

How Do You Like Them Schemas?

Now let us get into the schema side of things! Consider the following very simple object that contains some user data that we may require.

{

"name": "John",

"age": 43,

"gender": "male",

"phoneNumber": "0728903254",

"internationalCode": "+27"

}

As you can see, there is some simple data about a gentleman named John.

Let’s assume that the incoming name should definitely be a string. How would the JSON schema define this?

{

"name": {

"$id": "#/properties/name",

"type": "string"

}

}

Effortless and relatively concise. Likewise, if we were to assume that the age being provided would also be a number, we could define the type as integer .

{

"age": {

"$id": "#/properties/age",

"type": "integer"

}

}

This is all well and good, but what if we need to validate that the data conforms to a pattern more specific than a primitive type?

Let’s take a look at the international dialing code or internationalCode in the object we have above.

You can read up more about international dialing codes here ←

For this property, we may want to include some subtly more complex validation requirements. I.E we should check that perhaps there is a + prefixed to the code and that the code is a number that is between 1 and 4 digits long. Don’t fret! The schema has you covered!

{

"internationalCode": {

"$id": "#/properties/internationalCode",

"type": "string",

"pattern": "^\\+[0-9]{1,4}$"

}

}

You can see that we are now ensuring that the incoming request conforms to a specified pattern. Useful!

Let’s assume here for the sake of the exercise that there are only two valid genders that the incoming request may conform to, namely male and female. We could introduce an enumerated list as part of the validation requirement.

{

"gender": {

"$id": "#/properties/gender",

"type": "string",

"enum": [

"male",

"female"

]

}

}

Now suddenly our data is being more thoroughly validated for not only the existence of the property but also the content of the property!

If we were wanting to make the property optional or required, how would we go about doing this?

{

"required": [

"name"

]

}

Now the name property will be required and the rest of the properties, optional. If the name property does not exist, the validator will return an error message indicating that this property is required.

If we now combine all these snippets together into a single and valid schema, what would it actually look like?

{

"required": [

"name"

],

"properties": {

"name": {

"$id": "#/properties/name",

"type": "string"

},

"age": {

"$id": "#/properties/age",

"type": "integer"

},

"gender": {

"$id": "#/properties/gender",

"type": "string",

"enum": [

"male",

"female"

]

},

"phoneNumber": {

"$id": "#/properties/phoneNumber",

"type": "string"

},

"internationalCode": {

"$id": "#/properties/internationalCode",

"type": "string",

"pattern": "^\\+[0-9]{1,3}$"

}

},

"$id": "http://example.org/root.json#",

"type": "object",

"definitions": {},

"$schema": "http://json-schema.org/draft-07/schema#"

}

It’s vitally important to validate user input. AJV and JSON schemas make it rather easy to implement quite an effective and simplistic validation mechanism. I would highly recommend using AJV.

Useful Tools?

You might be thinking that creating a schema can be tricky and tedious… Wrong. It’s super easy, barely an inconvenience. There is an online tool that allows you to enter a normal JSON object and it will automatically generate a schema for you that will validate that object. You are then free to tweak it as much as you want.

For instance, you may not want all properties to be required. Some properties might need to conform to a pattern or even be part of an enumerated list.

BrijPad — JSON Schema Generator

Free Online JSON to JSON Schema Converter

JSON Schema

Schema Examples