TypeScript Validate JSON Example
May 27, 2022
It’s surprising to me how many times in my career I’ve had to validate the contents of a JSON file. Usually it’s for some configuration settings, or pre-loading data into a database. And yet I haven’t really found a painless way of doing it. This TypeScript example is probably the closest I’ve come to tolerable JSON validation, and even it isn’t without its pain points. Maybe I just need to accept the fact that validating JSON is annoying and there is no escaping that.
TypeScript seems like a good candidate to validate JSON because of it’s robust type system, and because JSON stands for JavaScript Object Notation after all. But no, most of the magic in using TypeScript to validate JSON doesn’t come from the language itself, but from third-party libraries that use decorators and meta-programming to infer types. In fact, the TypeScript team have been explicit about the fact that the language wasn’t designed to be used for data validation out of the box.
Working Solution
Below is small working example of how I’d validate JSON with TypeScript.
Dependencies & Important TS Flags
// package.json
...
"devDependencies": {
...
"typescript": "^4.6.4"
},
"dependencies": {
...
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2"
}
...
// tsconfig.json
{
"compilerOptions": {
...
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
...
}
}
Example Code
// config.json
{
"logLevel": "error",
"isPrimary": true,
"port": 80,
"rootPath": "/"
}
import config from "./config.json"
import { plainToInstance } from "class-transformer"
import { IsBoolean, IsInt, IsString, validate } from "class-validator"
class Config {
@IsString()
logLevel!: string
@IsBoolean()
isPrimary!: boolean
@IsInt()
port!: number
@IsString()
rootPath!: string
}
const configInstance: Config = plainToInstance(Config, config)
validate(configInstance).then(errors => console.log(errors))
Getting to the solution
The validation magic here is being done by the class-validator
library. The way this library works is you need to add its type decorators to each property based on what you’re expecting the type to be. For example, isPrimary: boolean
is expected to be a bool so I’d use class-validator’s IsBoolean
decorator to enforce that type. The same is true for any other built-in TypeScript type. The patten is @Is<type>()
, unless you’re doing something custom or nested, which I’m not here so I won’t go into it.
As the name class-validator
implies, this library performs validation against TypeScript classes. But, a JSON object is not a class. So, before I can use this library, I need to convert my JSON object to a class. This is done by using the class-transformer
library which is made by the same creator as class-validator, conveniently.
import config from "./config.json"
class Config {
logLevel!: string
isPrimary!: boolean
port!: number
rootPath!: string
}
Above I’ve defined a Config
class that is the same shape as my config.json
data. I’ve used TypeScrip’s definite assignment assertion for each property in the class since there isn’t a constructor
and the properties aren’t initialized elsewhere in the class. Which is interesting because at this point, each property definitely won’t be initialized by the class-transormer
library. For example, if the rootPath
key value pair was missing in the config.json
file the class-transformer
library would just create the class without that property. The rootPath
property would be undefined
.
So, the class at this point should technically look as follows:
class Config {
logLevel: string | undefined
isPrimary: boolean | undefined
port: number | undefined
rootPath: string | undefined
}
But, using class-transformer
is really just the first step in the process for the Config
class. I’m eventually going to be adding the class-validator
decorators to the properties which is going to make them required by default. Which is why the final version of the Config
class uses the definite assignment operator, which tells the compiler that the properties will eventually be given a value.
Now it’s just a matter of adding the correct decorator for each property type. I really wish this could be inferred, but I guess it’s just not meant to be. So, decorator it is.
Below is how the Config
class looks with the decorators added.
import config from "./config.json"
import { plainToInstance } from "class-transformer"
import { IsBoolean, IsInt, IsString } from "class-validator"
class Config {
@IsString()
logLevel!: string
@IsBoolean()
isPrimary!: boolean
@IsInt()
port!: number
@IsString()
rootPath!: string
}
const configInstance: Config = plainToInstance(Config, config)
console.log(configInstance)
// Config { logLevel: 'error', isPrimary: true, port: 80, rootPath: '/' }
Now I can use the class-transformer
library to create an instance of the Config
class with data from config.json
. This is done using the plainToInstance
method. Which is slightly annoying because the class-transformer docs use the plainToClass
method but in the source code itself that method is deprecated. But, either method works. I just wouldn’t put the code into production using plainToClass
.
The plainToInstance
method ignores the decorators and instantiates the class with the data from the config.json
file. Once the instance is created, I can then use the class-validator
validate
method to check if the class created based on the JSON is valid.
import config from "./config.json"
import { plainToInstance } from "class-transformer"
import { IsBoolean, IsInt, IsString, validate } from "class-validator"
class Config {
@IsString()
logLevel!: string
@IsBoolean()
isPrimary!: boolean
@IsInt()
port!: number
@IsString()
rootPath!: string
}
const configInstance: Config = plainToInstance(Config, config)
validate(configInstance).then(errors => console.log(errors))
If the JSON is valid, the validate
method will return an empty array because there aren’t any issues. If I change the JSON to be invalid by removing the required rootPath
key value pair from config.json
then I’d get an error object in the errors array from the validate
method.
// Invalid JSON according to the Config class definition
{
"logLevel": "error",
"isPrimary": true,
"port": 80
}
Error response from the above invalid JSON is shown below. Note that the target
class is missing the rootPath
property.
[
ValidationError {
target: Config { logLevel: 'error', isPrimary: true, port: 80 },
value: undefined,
property: 'rootPath',
children: [],
constraints: { isString: 'rootPath must be a string' }
}
]
Aside from the Config
class definition, it’s really just two lines of code to get basic JSON validation working with TypeScript. It did require external libraries, but that’s to be expected with most JavaScript projects these days. Both the class-validator
and class-transformer
libs have a huge amount of features worth exploring. An interesting feature of class-validator
worth pointing out is the fact that the type decorators aren’t a strict requirement, and a JSON config with expected type definitions can be used instead. Which is pretty cool.