This article has been translated from the original article by the author himself using DeepL translator. Thanks, www.deepl.com !
TL; DR
- Introduce Ajv JSON Validator
- and adopt JSON Typed Definition
- Cross-platform typed definitions and validators at your fingertips!
Note: This article you reading has been written with reference to this page.
Introduction
It is adopted as a common standard for both the front-end and the back-end, and is universally understandable and writable by many people ―――
In programming, data structures can take many forms, depending on their use; e.g. CSV / XML / YAML / TOML. While there are many examples of these, none are as widespread or as easily understood by everyone as JSON. It is often used for human reading and writing, as well as for client-server interaction.
On the other hand, its flexibility also makes it difficult to handle safely: When communicating with an arbitrary API server and receiving data, it's common to give up defining type of the response and treat it as any
type. In this way, the data can be received, but the subsequent handling will be difficult.
This is when you want a validator, a device that verifies and guarantees the correctness of your data. Validators can be used to detect and successfully handle property excess or deficiency and invalid values.
Of course, it's possible to code it yourself from scratch, for the sake of experience, but we'd rather use a framework that already exists ! In this article, introducing Ajv as a JSON validator library for JavaScript / TypeScript. I would also show you how to define a innovative schema, called JSON Typed Definition.
Note: JSON Typed Definition is proposed in RFC8927 and its current status is Experimental
.
What is Ajv ?
Ajv is a JSON validator used in a general JavaScript environment. It has the following three features : Ensure your data is valid as soon as it's received Instead of having your data validation and sanitization logic written as lengthy code, you can declare the requirements to your data with concise, easy to read and cross-platform JSON Schema or JSON Type Definition specifications and validate the data as soon as it arrives to your application. TypeScript users can use validation functions as type guards, having type level guarantee that if your data is validated - it is correct. Compiles your schemas to optimized JavaScript code Ajv generates code to turn JSON Schemas into super-fast validation functions that are efficient for v8 optimization. In the early days it was very popular for its speed and rigor, but it also had many security flaws. However, over the years and with the help of many user reports, these flaws have been fixed and secure code generation has been re-established in v7. Use JSON Type Definition or JSON Schema In addition to the multiple JSON Schema drafts, including the latest draft 2020-12, Ajv has support for JSON Type Definition - a new RFC8927 that offers a much simpler alternative to JSON Schema. read more ...
1. Write less code
2. Super fast & secure
3. Multi-standard
Designed to be well-aligned with type systems, JTD has tools for both validation and type code generation for multiple languages.
Before you start with Ajv ...
On JTD, the schema is defined using eight different forms e.g. Properties
/ Elements
/ Values
etc.. If you would like to know more about these first, please see this article.
Getting started with Ajv on JTD
So let's get started with Ajv!
First of all, we need to install the npm
package:
npm install ajv
# or
yarn add ajv
Before you can actually validate in Ajv, you need to go through the following steps:
- define your schema on JSON Typed Definition
- get the type definition from the schema object by using "utility types"
- initialize the
Ajv()
constructor (and do some configurations) - get a validator (+ ) from the type definition
Let's look at them in turn!
1. Define your schema on JSON Typed Definition
First, here is an example of a schema definition on JSON Typed Deifinition.
const schema = {
properties: {
foo: { type: "int32" },
},
optionalProperties: {
bar: { type: "string" },
},
} as const;
Some of you may have noticed as const
at the end of the code. This feature is called const assertion, implemented since TypeScript 3.4.
TypeScript has a feature called type assertion by more descriptions ...
as
, which explicitly overriding an inferred type. In the case of const assertion, which is an extension of type assertion, it causes the following effects:
// Type '"hello"'
let x = "hello" as const;
// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;
// Type 'readonly [10, 20]'
let y = [10, 20] as const;
Thanks to this feature, the schema is cast as follows:
const schema: {
readonly properties: {
readonly foo: {
readonly type: "int32";
};
};
readonly optionalProperties: {
readonly bar: {
readonly type: "string";
};
};
};
All properties are now readonly
, and each type
property is now defined as a literal ("int32"
, "string"
) instead of string;
!
2. Get the type definition from the schema object by using "utility types"
Now, we have the variable schema
, which is type-asserted via as const
. What if we further apply the typeof
operator to this variable? It is also used in type guards, which are often used to determine undefined
; that is, the typeof
operator is used to get the type of a variable schema
.
type MyData = JTDDataType<typeof schema>;
Next, typeof schema
is passed as an argument to JTDataType<T>
, which is provided by the Ajv as utility types .
Utility types are types that derive another type from a type; if functions are functions in the runtime world, then utility types are functions in the type world.
cf. book.yyts.org
That is, JTDataType<T>
convert from the argument typeof schema
to MyData
. At this point, this type definition has been cast as follows:
type MyData = {
foo: number;
} & {
bar?: string | undefined;
};
🤔?🤔?🤔?🤔?🤔?🤔?🤔?🤔?🤔?🤔?🤔?🤔?
_人人人人人人人人人人人人人人人人人人_
> When we defined the schema in JTD, <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
_人人人人人人人人人人人人人人人人人人人人_
> we also finished defining the TS typedefs! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
3. Initialize the Ajv()
constructor (and do some configurations)
Let's calm down for a moment and think about the following ...
// import Ajv from "ajv"; // <----- not for JTD
import Ajv from "ajv/dist/jtd";
const ajv = new Ajv();
Here, we import the necessary modules and initialize the constructor. Note that the source of this import is ajv/dist/jtd
, since JTD is used. If you want to add more settings, you can pass Option as an argument to the constructor.
/*
* For example, to report all validation errors (rather than failing on the first errors)
* you should pass allErrors option to constructor:
*/
const ajv = new Ajv({allErrors: true})
4. Get validator and + from the type definition
The compile
constructor method can be used to convert a schema definition into a validator function.
// type inference is not supported for JTDDataType yet
const validate = ajv.compile<MyData>(schema);
What a surprise! 🤣 Just by defining the schema on JTD, we got a TypeScript typedef and a validator that uses it.
const validData = {
foo: 1,
bar: "abc",
};
if (validate(validData)) {
// data is MyData here
console.log(validData.foo); // 1
} else {
console.log(validate.errors);
}
This is the beauty of Ajv on JTD. The amount of writing is much less, just like the goal of Write less code.
And there's more good news! Ajv also provide compileParser
and compileSerializer
to meet the demand for a "more type-safe" parser and serializer. In other words, we can now do JSON.parse()
/ JSON.stringfy()
in a more type-safe way.
const parse = ajv.compileParser<MyData>(schema);
const serialize = ajv.compileSerializer<MyData>(schema);
how to parse / serialize
const data = {
foo: 1,
bar: "abc",
};
const invalidData = {
unknown: "abc",
};
console.log(serialize(data));
console.log(serialize(invalidData)); // type error
const json = '{"foo": 1, "bar": "abc"}';
const invalidJson = '{"unknown": "abc"}';
console.log(parseAndLogFoo(json)); // logs property
console.log(parseAndLogFoo(invalidJson)); // logs error and position
function parseAndLogFoo(json: string): void {
const data = parse(json); // MyData | undefined
if (data === undefined) {
console.log(parse.message); // error message from the last parse call
console.log(parse.position); // error position in string
} else {
// data is MyData here
console.log(data.foo);
}
}
Excellent!
JSON Typed Definition Validator
"OK, I'll give JTD a go!", this thought occurred to me at the end of last month. However, at the time, I had no idea how to define scheme on JSON Typed Definition. I even tried to translate the official explanations into my native language to get a better understanding, but it didn't feel quite right. Or, to start with, it was bad for me because I didn't know if the schema definition was correct or not until I actually ran the code.
🤮
I couldn't just give up here, so I decided to build my own webApp using Next.js, which I've been working on a lot lately.
Please try it! And I would love it if you gave me a star or something! 🥳
Conclusion
So far, we have actually done the following two things:
- Schema definition based on JSON Typed Definition
- Initial configuration of Ajv
Here's what we got out of these:
- typedefs in TypeScript derived from JTD
- type-safe Validator based on 1.
- type-safe Parser based on 1.
- type-safe Serializer based on 1.
Haha! After all the hard work and effort in the past, we can now get """everything""" just by defining the schema on JTD. 🥲
I hope you enjoy the best schema-driven development tomorrow with JSON Typed Definition and Ajv JSON Validator! 👍
Top comments (0)