In the spirit of parse, don’t validate, I’ve started using zod as my go-to JSON parsing library in Typescript.
JSON.parse
, or something like response.json()
gives you an valid JS object
but in your application you’re usually working with a richer set of types that
will almost always require validation. The idea is to parse the JS object into a
Typescript object.
While there are powerful parser combinator libaries for JavaScript, zod takes a simpler approach by only allowing you to JSON objects1 and only exposing a few (and very sane) set of parsers for each field. And of course, you can use to parse any JSON object, not just those received from network requests.
Schema
Consider the typescript type
type Workout = {
id: string;
kind: WorkoutKind;
reps: number;
duration_seconds: number;
user_id: string;
order: number;
};
type WorkoutKind = "pushups" | "burpees" | "squats";
We will write a zod
parser that parses JSON value representing a workout.
export const workoutSchema = z.object({
kind: z.enum(["pushups", "burpees", "squats"]),
reps: z.number().positive().lt(100).int(),
duration_seconds: z.number().positive().int(),
user_id: z.string(),
order: z.number().positive().int(),
id: z.string(),
});
And parse with
try {
const workout = workoutSchema.parse(responseObject);
} catch (e) {
console.log(e);
}
Actually, you don’t need to define the Workout
type as you can use zod’s
infer
generic type to derive the resulting object’s type.
type Workout = z.infer<typeof workoutSchema>;
API schemas
I like to work with a standard format for API responses as much as possible.
Something like the following has the actual data (on success) wrapped in a
data
field.
export interface JsonResponseBody<DataT> {
data: DataT;
errorCode: string;
errorMessage: string;
}
Let’s say that we have an API GET /workouts
which returns a list of workouts.
The schema for the response object getWorkoutsResponseSchema
can be written
like so
// Helper function that creates the top-level response schema with
// data field schema given as an argument
function makeApiResponseSchema<DataT>(dataObjectSchema: z.Schema<DataT>) {
return z.object({
data: dataObjectSchema,
errorCode: z.string(),
errorMessage: z.string(),
});
}
// The schema for the response of GET /workouts API
export const getWorkoutsResponseSchema = makeApiResponseSchema(
z.object({
workouts: z.array(workoutSchema),
})
);
The actual parsing can be done with something like
try {
const workout = getWorkoutsResponseSchema.parse(await response.json());
} catch (e) {
// Handle parse error
}
-
Anything that is the result of a
JSON.parse
. ↩︎