Using Zod for parsing data

Photo by Sam Barber on Unsplash

Using Zod for parsing data

A steeping stone to Algebraic Data Types

I have had to take a data structure multiple times in my career and go from version 1 to version 2. Most times, this involves updating two different code bases(like the frontend and the API) so that they both agree to this new data format. A few unlucky times, I would receive the old and new data structures and had to handle both.

Recently I have been enamored with a Typescript library named Zod and will use that as a stepping stone to help show the importance of data structures and algebraic data types. This is common with languages like Elm and its union type and parsers but uncommon in Typescript. It helped open my eyes to how powerful and why you should Parse Don't Validate.

This blog post assumes some knowledge of libraries like Zod that parses (like Joi) but also creates a Typescript type. Read the start of the Readme for a better feel the goals of Zod.

First, let's set up an example. Maybe you have some unversioned data, but it does have a different shape. We will create a Blog Post Data structure that will change from having tag be a single string to an array of strings called tags.

type BlogPostv1 = {
  title: string,
  body: string,
  tag: string,
}

const BlogPostv2 = {
  title: string,
  body: string,
  tags: string[]
}

So let's recreate these types in Zod.

import { z } from "zod";

const BlogPostv1 = z.object({
  title: z.string(),
  body: z.string(),
  tag: z.string()
});


const BlogPostv2 = z.object({
  title: z.string(),
  body: z.string(),
  tag: z.array(z.string())
});

const BlogPost = z.union([BlogPostv1, BlogPostv2]);

We created a third type that is either a v1 blog post or a v2 blog post. This can be done in vanilla Typescript but now with Zod we can use BlogPost like a validation function also. To find out which version after parsing, we will need to have an if statement based on tag vs tags.

const rawBlogPost = {"title":"...", "body": "...", "tag" : "..."}
const data = BlogPost.parse(rawBlogPost)

//Assume version 1
if(data.tag) {

//Assume version 2 as default
} else {

}

We can use one other trick to help identify which version by using the optional and default statements in Zod.

const BlogPostv1 = z.object({
  version: z.literal(1).optional().default(1),
  title: z.string(),
  body: z.string(),
  tag: z.string()
});


const BlogPostv2 = z.object({
  version: z.literal(2).optional().default(2),
  title: z.string(),
  body: z.string(),
  tag: z.array(z.string())
});

const BlogPost = z.union([BlogPostv1, BlogPostv2]);

This does change the data structure slightly, but when it parses the data structure, it will append a version number automagically so we can do the following if statement.

const rawBlogPost = {"title":"...", "body": "...", "tag" : "..."}
const data = BlogPost.parse(rawBlogPost)

//typechecks
if(data.version === 1 ) {
// ...
   data.tag
} else if (data.version ===2) {
// ...
   data.tags
}

This technique helps leverage the tooling of Typescript so that you have an easier time dealing with the data. Often the boundaries and types of data are a major problem, such as in the Mars Climate Orbiter , and both Typescript and Zod can help with this. Look for opportunities to mix your validations with the type system and you will start to see the power a library like Zod can give you.