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
}

  1. Anything that is the result of a JSON.parse↩︎