Most software needs to consume data from an external source. When writing such software, it’s good practice to validate the incoming data to ensure that it’s in an expected format. In a language like Typescript, it generally implies the need to create types that describe the structure of that data as well. As with most things in software (and life), there are multiple ways to do this.
One option is to manually create types that describe the data you expect. Then, create functions that check the raw data to see if it can be cast to the type you’ve defined. This works fine but can break down when 1) the data’s structure becomes complex, or 2) the validity checks become more nuanced (eg. string length, number min/max value, specific enums etc…). While attempting to create robust yet generic validation functions, you might quickly find yourself writing your own validation library. Thankfully there are already libraries that do all of this and do it well!
What are my validation library options?
One library I’ve used in the past for this sort of thing is called AJV. By using AJV along with json-schema-to-ts you can write JSON in a specific structure, called json-schema, that can be used to both validate your data and generate Typescript types. This is a significant improvement on the manual approach as you no longer need to define types yourself. Instead, they are derived from the json-schema. Additionally, you can avoid writing your own validators as you are able to characterize what makes data “valid” via the json-schema.
This is pretty great overall, but AJV’s json-schema based approach has limitations. Recently, I learned about a compelling alternative called Zod.
Before I dive into Zod, I want to highlight that AJV is not a tool for generating types. It only validates JSON based on json-schema, an area where it excels. The type generation I allude to is a result of using json-schema-to-ts along with AJV.
What is Zod and why is it awesome?
Zod is a validation library that allows you to define schemas in code. This differs from AJV’s json-schema based approach, and it’s this difference that ultimately leads to a significant boost in developer friendliness. By defining our schema in code, we enable the magic of Typescript’s type inference, which can statically derive the type of the validated data. This allows us to use types based on schemas throughout the codebase immediately – which is very cool.
This contrasts with type generation of a json-schema based approach where generating types requires an additional compilation step each time the schema is edited. You can automate this with a tool like Watchman, but there’s a cost to setting this up and maintaining it. With Zod, you never have to think about it.
With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type.
Aside from static type inference, I’ve also found the developer ergonomics of working with Zod to be notably better than a json-schema based approach. I attribute this to Zod’s interface, which is very intuitive unless you’re doing something custom. Even then it’s simple, but you’ll likely need to dive into the docs. Overall, Zod’s validation functions are straightforward and it’s a big plus to have autocomplete built-in thanks to building schemas within code.
With Zod, you can also compose schemas and structure them in ways that closely mirror the structure of the data. This makes it fairly easy to create complex schemas that are more comprehensible and less verbose. If you want to read more about validation with Zod, check out Gage’s post here.
One downside of defining your schemas in code is that they can’t be hosted and consumed like a json-schema can. Fortunately, for anyone interested, there are libraries to convert Zod to json-schema and vise-versa. You can choose from a slew of other libraries, including integrations with popular form libraries such as React Hook Form (which I highly recommend) and Formik.
I’d strongly urge anyone looking for better validation in Typescript to give Zod a try.