Kneel Before Zod: Improving Validation for Your TypeScript Projects

Mario Bittencourt
SSENSE-TECH
Published in
7 min readSep 23, 2022

Validation is present in all services we develop at SSENSE as part of the execution pipeline. It ensures the various inputs received conform with expectations in structure and value. This is true from the original data received from the user of the service — be it via API or messaging — all the way to its internal components — functions and classes — as well as interactions with other services.

This is one of the motivations for choosing a language that supports a type system, and at SSENSE we use TypeScript. While trying to leverage its capabilities I came across certain limitations in comparison to other strongly-typed languages, especially when it comes to runtime guarantees.

In this article, I will share some of these limitations and discuss Zod, one alternative that enhances the necessary validation.

Not all Types are Created Equal

When approaching Typescript my first expectation was that its type system would behave in the same way as languages like Java or C#. I would define a class or type, specify a variable is that type, and voilá, get the guards[1] during compile and execution alike.

Surprise! I do not work like that! — Typescript

Typescript only checks for the type at development and compile time, not during runtime.

In this example we see the IDE indicating that there is something wrong with the information, represented by the red underline in the variable’s definition.

But the above code will pass even if the event does not have the expected data structure.

While debating if this is a good or bad thing is outside the scope of this article, solving this shortcoming is something that I feel is valuable because it helps to provide early validity to the data you will manipulate within your application[2]. The first step towards finding our solution is to determine where to fix it.

Thou Shall not Pass

Determining where to validate largely depends on the architecture style you adopt. In my case, when using hexagonal architecture you can identify primary and secondary adapters at the edges of the application where your application receives its inputs or interacts with external dependencies.

Figure 1. Hexagonal architecture. Click here to expand the image. Source: Herberto Graca.

Even if you do not follow hexagonal architecture you can relate those two interaction points as where you receive potentially bogus data.

So, preventing bad data from entering your service at those points is the first line of defense in a multi-level approach.

No, I am not Talking About DDD

It is no secret that at SSENSE we like using Domain-Driven Design (DDD) in our applications and I’m a big proponent of this approach. I try to selectively apply DDD patterns when applicable. When talking about validation one could think: here he goes again trying to sell Entities and Value-Objects to me!

While leveraging these tactical elements should not be ruled out, they reside at the domain level, and as I presented in the previous section we should focus first on the UI and infrastructure layers (primary and secondary adapters respectively).

A Recurring Problem

Imagine we have a service that is responsible for placing orders on an e-commerce site. It receives the request and as part of the process it needs to check if the items selected have stock prior to continuing.

Figure 2. An API-backed service with an external dependency.

Let’s look at a simple implementation using the information that’s been received.

The code above uses the data from the input — which is supposed to contain an SKU[3] — to process the information and call an external stock service to check the current stock level prior to continuing.

The problem here is that assignment is not enough to know for sure if we will actually receive the information we are looking for. Only when you try to access the properties that do not exist will the error manifest itself.

Similarly, we have the same issue with the external dependency interaction.

To protect us from this, we could assess if the information is available before trying to refer to it.

While this works, I had to mark all the properties as optional, even though they are not. I also had to manually check every property myself. This is ok if you have a handful of properties, but can get tedious if you have a more complex nested object structure.

There should be another way, a better way, to achieve the same result.

Zod to the Rescue

Zod is a small schema declaration and validation library with first-class Typescript support. Let’s see the basics of using it:

Besides the string and number, Zod offers boolean, date and null among the primitive types. To determine if a specific value is acceptable, you can use the parse method.

Since our inputs are rarely as simple as the ones seen so far, we have to see how a complex schema can be captured:

In the same fashion, we can use parse and determine if the received input is correct or not.

Now that we know the basics let’s revisit the example API this time using Zod.

So now, prior to any transformation or processing of the information, you know that it conforms to the structure you would expect.

But we can do better. Notice that even though we know if the received information adheres to the schema, I do not have the benefit of the type definition, be it for compilation or defining the functions or methods signatures. To address this, Zod offers the capability to generate the type declaration for us.

This is much better as you do not have to duplicate the definition of the schema using Zod and a plain type. Instead, you generate, or infer, the type based on the Zod schema.

But I Already Have a JSON Schema Spec

In the example we addressed, our service exposes an API and calls an external dependency or service, which is also exposing an API.

Imagine that in both cases the API contract is defined using a JSON schema. If that is the case, the Zod ecosystem offers some helpful tools to allow you to take an existing definition and generate a Zod schema.

Would generate this Zod schema:

This allows you to reduce the manual overhead of defining your Zod schema. Similarly, there are other tools that can do the reverse, take a Zod schema and output a JSON or OpenAPI version.

Conclusion

Validation is often overlooked by developers. We’re optimistic by nature and TypeScript can lull us into a false sense of security as we define our types despite the fact that it only gives some guarantees. If you come from other strongly-typed languages, you may be surprised to see production bugs in what you assumed was a safe Typescript service.

Since Typescript only offers type validation at compile time, every time you are dealing with dynamic inputs you have to take care of the validation yourself.

While you can definitely do it without leveraging any library, Zod is an interesting solution given its Typescript support, small footprint, and number of features.

This article only scratched the surface of what it can do, so I encourage you to evaluate how you approach the validation at the UI and infrastructure levels, and see if Zod can provide you with the guarantees your service needs.

Finally, there are other alternatives to Zod, such as ajv and joi, but independently of which one you choose, consider leveraging the approach to provide strong structural guarantees to the primary and secondary drivers to reject invalid data. Your application and domain code will thank you for it!

Stay safe! :)

Additional Notes:

[1] — Guards — In computer programming, a guard is a boolean expression that must evaluate to true if the program execution is to continue in the branch in question.

[2] — Fail fast — a design proposal that proposes to expose failures immediately

[3] — SKU — Stock Keeping Unit is a code used to track the movement of inventory

Editorial reviews by Catherine Heim & Nicole Tempas.

Want to work with us? Click here to see all open positions at SSENSE!

--

--