Zod is dead. Long live ajv-ts!
Vitali Haradkouvitalics
What is Zod?
Zod is a schema-level validation library with first-class Typescript types support.
I want examples! Okay, here you are.
Let’s imagine that we want to create an object User
with email and password fields. Both are them - required.
Zod will handle incoming arguments into the parse
function and throw if argument not match the schema
Why Zod does not work for us?
In the current project, we use ajv
schema validator. Since Zod has it’s own validation, it does not match our requirements.
But! Our models are JSON-schema compatible - My thoughts. This is an entry point of my adventure which has the name ajv-ts
.
Attempt 1. Types
First of all, I define the base type:
based on JSON-schema specification - I define primitives(Number, String, Boolean, Null) and complex(Object, Array) types
Example of the Number schema:
And Also I define AnySchema
which takes all possible schema types:
Attempt 2. Builder
The next step is to define Builder. But some methods should be inherited. So I made the decision to create a class SchemaBuilder
.
It has next signature
And you may ask:
- Why class is an abstract?
- Because we don’t need to allow the creation of a
SchemaBuilder
instance - Why do you need to define
Output
generic? - Transformers! - my answer.
- What are the
transformers
?
Well, transformers
- a function that transforms input or output results. It works as hooks before or after the safeParse
method.
The parse
method uses safeParse
to not throw any errors
Let me show you an example:
How it will used:
And the Same for postprocess
. The idea is simple. the parse
method returns the Output
generic.
Input is an “incoming type” and is used to define input arguments.
Example with a NumberSchemaBuilder
Input
generic defines number
type.
The idea is manipulation with JSON-schema, since many validators understand JSON-schema - it’s a standard de facto!
BUT: zod has its own-written parser and it does not respect JSON-Schema notations. hello bigint
, function
, Map
, Set
, never
functions.
Bonus: Define the number
function:
Attempt 3. Infer parameters
How Zod
understands which type is your schema. I mean how z.infer<Schema>
works?
As you may found Output
is an exact type that we need because it respects typescript parser output result.
That means you only need to call Output
generic, but how it’s possible? NumberSchema
does not have such a parameter, only SchemaBuilder
.
The answer is tricky - we can define “empty” by value type and set its incoming generic input or output.
Let’s switch it up to SchemaBuilder
again and define a “tricky” hack!
The _input
and _output
is always have “undefined”, but we will use them to infering parameters.
Now we can check that this is works:
Gotha! BTW zod
works in the same way. I cannot find any other solution.
Attempt 4. All Together
Final thoughts
The main point is achieved - we define JSON-schema builder with first-class typescript type inferring! And it’s awesome!
The library has the same API as Zod
. Now any team in the project can simply define schemas. Here is a few example, more on the project readme:
We also use our builder to create a “swagger” like schema. In codegen:
The output will be:
Okay, it’s time to stop. My final words will be a comparison between our solution and zod
.
Advantages:
- Meets our requirements and expectations.
- Fewer lines of code.
zod
has more than 4k lines in the main file (src/types.ts
) while we have only 1k lines of code. ajv-ts
respects JSON-Schema since it’s standard. JSON-schema is available under theschema
property and is easy to generate output.- uses
ajv
under the hood which reduces our bundle size and cost of support for validation.
Disadvantages:
- Cost of support and buggy(while we are not in stable release)
- Types overload, but Zod has the same issue.
- Not world spread, we just released our solution
- we are not fully compatible with
zod
. But in most cases, you can reimport without any problems(if you are not using transformations or custom errors) - Custom errors are not supported.