Background image

Building Speakeasy

Writing Zod code that minifies

Georges Haidar

Georges Haidar

June 5, 2024

Featured blog post image

I don't think it's exaggerating when I say that Zod (opens in a new tab) has had a major positive impact on how we write safe TypeScript code that also preserves its safety guarantees at runtime. It's so powerful that we at Speakeasy based the design of our latest TypeScript SDK generator on it.

We've been iterating on the TypeScript code we're generating because we're going through a phase of optimising the size of our SDKs so they work even better in the browser and one interesting area has been around how we write Zod schemas.

Consider the following (contrived) example of a Zod schema:


import * as z from "zod";
export const person = z.object({
name: z.string(),
age: z.number().optional(),
address: z.object({
line1: z.string().optional(),
line2: z.string().optional(),
line3: z.string().optional(),
line4: z.string().optional(),
city: z.string(),
}),
pets: z.array(
z.union([
z.object({
type: z.literal("dog"),
name: z.string(),
}),
z.object({
type: z.literal("cat"),
name: z.string(),
}),
])
),
});

This is a fairly common style of writing zod schemas: you import the whole library as a single variable and use the powerful chaining API to describe validation rules.

When running this code through a bundler like esbuild we're going to get subpar minification performance. Esbuild will remove unnecessary spaces and newlines but I'm going to keep them in for the sake of readability. Here's the formatted result:


import * as t from "zod";
export const person = t.object({
name: t.string(),
age: t.number().optional(),
address: t.object({
line1: t.string().optional(),
line2: t.string().optional(),
line3: t.string().optional(),
line4: t.string().optional(),
city: t.string(),
}),
pets: t.array(
t.union([
t.object({ type: t.literal("dog"), name: t.string() }),
t.object({ type: t.literal("cat"), name: t.string() }),
])
),
});

The actual minified code comes out to 379 bytes, down from 512 bytes. That's rougly a 26% reduction.

Notice how not a lot has changed. In fact, the only thing that has changed is that esbuild renamed z to t. The only net reduction in this minified code can be attributed to the removal of unnecessary whitespace characters which we've kept in here.

So on the current course, if your project is building up more and more Zod schemas, you'll notice that the minified code isn't tremendously smaller than the unminified code. The only real gains will be from compressing this code with gzip or brotli (or whatever you prefer) but that doesn't impact the size of the code that needs to be parsed. Furthermore, minified can still compress well and result in overall less text to send down the wire and to parse.

The way to improve the impact of minification on our code will come from using local variables that can be rewritten by the minifier. Fortunately, Zod exports much of its API as a standalone functions that can be separately imported.

Lets rewrite the code above using these standalone functions:


import { object, string, number, array, union, literal, optional } from "zod";
export const person = object({
name: string(),
age: optional(number()),
address: object({
line1: optional(string()),
line2: optional(string()),
line3: optional(string()),
line4: optional(string()),
city: string(),
}),
pets: array(
union([
object({
type: literal("dog"),
name: string(),
}),
object({
type: literal("cat"),
name: string(),
}),
])
),
});

... and the minified result:


import {
object as t,
string as e,
number as i,
array as o,
union as r,
literal as a,
optional as n,
} from "zod";
export const person = t({
name: e(),
age: n(i()),
address: t({
line1: n(e()),
line2: n(e()),
line3: n(e()),
line4: n(e()),
city: e(),
}),
pets: o(
r([t({ type: a("dog"), name: e() }), t({ type: a("cat"), name: e() })])
),
});

That did better. The actual result is now 290 bytes, down from 512 bytes. That's roughly a 43% reduction!

You can see how the minifier was able to re-alias all the imports to single letter variables and use that to shrink the remaining code.

The new style of writing Zod schemas might be a little more tedious because you are no longer carrying the entire library with you with a single variable. However, if optimising for bundle size is a real concern for you, then this is a neat trick to keep in your backpocket.

CTA background illustrations

Speakeasy Changelog

Subscribe to stay up-to-date on Speakeasy news and feature releases.