---
title: "Mapping data with Zod"
description: "Use Zod to validate and reshape data from different sources with TypeScript."
canonical_url: "https://stijnvanhulle.be/blog/mapping-of-data-with-zod"
---

> Index of every page on this site: [llms.txt](https://stijnvanhulle.be/llms.txt)

## The problem

Mapping data in JavaScript and TypeScript is not always straightforward. You may need a series of checks or switch statements to determine which shape the input has, then write validation for each shape.

Getting the types right adds another layer of work. The usual tricks can make the mapper harder to understand and maintain.

The result is a lot of code around a fairly simple job. [Zod](https://zod.dev/) can handle the validation while you map the data.

```typescript
const user = {
  id: '123',
  firstName: 'Stijn',
  lastName: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
} as const

const twitterUser = {
  id: '123',
  first: 'Stijn',
  last: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
} as const

const googleUser = {
  uuid: '123',
  info: {
    firstName: 'Stijn',
    lastName: 'Van Hulle',
  },
  email: 'stijn@stijnvanhulle.be',
} as const

// no type support
const mapData = (data: any) => {
  if (data.info) {
    // google user
    return {
      id: user.uuid,
      value: user.info.firstName + ' ' + user.info.lastName,
    }
  } else if (data.first && data.last) {
    return {
      id: user.id,
      value: user.first + ' ' + user.last,
    }
  } else if (data.firstName && data.lastName) {
    return {
      id: user.id,
      value: user.firstName + ' ' + user.lastName,
    }
  }
}
mapData(googleUser) /* => {
        id: '123',
        value: 'Stijn Van Hulle'
    }
*/
```

## What is Zod?

Zod describes itself as "TypeScript-first schema validation with static type inference."

You can use Zod to validate forms, define schemas, and validate API input. [tRPC](https://trpc.io/) is one example of a library that uses Zod. Define a schema on the back end, then reuse it on the front end when making API calls.

There are many TypeScript [schema validation libraries](https://zod.dev/?id=comparison), but Zod also infers TypeScript types from your schemas. That makes it a good fit for a typed data mapper.

### Zod primitives

The following example checks whether a value is a string.

```typescript
import { z } from 'zod'

// creating a schema for strings
const mySchema = z.string()

// parsing
mySchema.parse('tuna') // => "tuna"
mySchema.parse(12) // => throws ZodError

// "safe" parsing (doesn't throw error if validation fails)
mySchema.safeParse('tuna') // => { success: true; data: "tuna" }
mySchema.safeParse(12) // => { success: false; error: ZodError }
```

### Zod object

Zod can also validate objects against a defined shape.

```typescript
import { z } from 'zod'

const User = z.object({
  username: z.string(),
})

User.parse({ username: 'Ludwig' })

// extract the inferred type
type User = z.infer<typeof User>
// { username: string }

User.parse({ firstname: 'firstname' }) // => throws ZodError
```

### Zod transform

Zod can transform a value from one shape into another. This example extracts the domain from an email address.

```typescript
const emailToDomain = z
  .string()
  .email()
  .transform((val) => val.split('@')[1])

emailToDomain.parse('colinhacks@example.com') // => example.com
```

## Mapping data

### Simple Zod mapper

With the basics in place, we can build a mapper schema using Zod primitives and `transform`.

1. Start with an input object and map it to an object containing only *id* and *value* (the user's full name).

```typescript
const user = {
  id: '123',
  firstName: 'Stijn',
  lastName: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
}
```

1. Use `transform` to create the mapped object.

```typescript
import z from 'zod'

export const userSchema = z
  .object({
    id: z.string(),
    firstName: z.string(),
    lastName: z.string(),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.id,
      value: user.firstName + ' ' + user.lastName,
    }
  })
```

1. Call `.parse` with *user*. It returns the mapped data.

```typescript
userSchema.parse(user) /* => {
        id: '123',
        value: 'Stijn Van Hulle'
    }
*/
```

### Multiple inputs mapper

What if the inputs have different shapes and schemas?

```typescript
import z from 'zod'

const user = {
  id: '123',
  firstName: 'Stijn',
  lastName: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
}

const twitterUser = {
  id: '123',
  first: 'Stijn',
  last: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
}

export const userSchema = z
  .object({
    id: z.string(),
    firstName: z.string(),
    lastName: z.string(),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.id,
      value: user.firstName + ' ' + user.lastName,
    }
  })

export const twitterUserSchema = z
  .object({
    id: z.string(),
    first: z.string(),
    last: z.string(),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.id,
      value: user.first + ' ' + user.last,
    }
  })

userSchema.parse(user) /* => {
        id: '123',
        value: 'Stijn Van Hulle'
    }
*/

twitterUserSchema.parse(twitterUser) /* => {
        id: '123',
        value: 'Stijn Van Hulle'
    }
*/
```

### Multiple unknown inputs mapper

What if the input shape is unknown?
Let's say we have 2 API calls:

- One to our database
- One to an external API(for example Twitter).

We could check where the data came from, but that brings back the same maintenance problem. We would still need conditional logic to inspect the input before converting it.

```typescript
import z from 'zod'

const user = {
  id: '123',
  firstName: 'Stijn',
  lastName: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
}

const twitterUser = {
  id: '123',
  first: 'Stijn',
  last: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
}

export const userSchema = z
  .object({
    id: z.string(),
    firstName: z.string(),
    lastName: z.string(),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.id,
      value: user.firstName + ' ' + user.lastName,
    }
  })

export const twitterUserSchema = z
  .object({
    id: z.string(),
    first: z.string(),
    last: z.string(),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.id,
      value: user.first + ' ' + user.last,
    }
  })

const mapData = (data: any) => {
  if (data.first) {
    // twitter
    twitterUserSchema.parse(data)
  } else {
    // database user
    return userSchema.parse(data)
  }
}

mapData(twitteruser) /* => {
        id: '123',
        value: 'Stijn Van Hulle'
    }
*/

mapData(user) /* => {
        id: '123',
        value: 'Stijn Van Hulle'
    }
*/
```

### Zod unions

This works for two sources, but what happens when a third source appears, such as Google user data? Adding another conditional would make the mapper grow with every source.

Instead, let Zod try each schema and infer the matching output type.

Zod unions, including the `.or` method, let you describe these alternatives in one schema.

```typescript
const schema = z.string().or(z.number()) // string | number
// equivalent to
z.union([z.string(), z.number()])

stringOrNumber.parse('foo') // passes
stringOrNumber.parse(14) // passes
```

### End result

Combine unions with transforms, and Zod can find the matching schema and return one consistent shape.

So with our previous example, we can simplify that to the following:

```typescript
const user = {
  id: '123',
  firstName: 'Stijn',
  lastName: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
} as const

const twitterUser = {
  id: '123',
  first: 'Stijn',
  last: 'Van Hulle',
  email: 'stijn@stijnvanhulle.be',
} as const

const googleUser = {
  uuid: '123',
  info: {
    firstName: 'Stijn',
    lastName: 'Van Hulle',
  },
  email: 'stijn@stijnvanhulle.be',
} as const
```

```typescript
import z from 'zod'

export const userSchema = z
  .object({
    id: z.string(),
    firstName: z.string(),
    lastName: z.string(),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.id,
      value: user.firstName + ' ' + user.lastName,
    }
  })

export const twitterUserSchema = z
  .object({
    id: z.string(),
    first: z.string(),
    last: z.string(),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.id,
      value: user.first + ' ' + user.last,
    }
  })

export const googleUserSchema = z
  .object({
    uuid: z.string(),
    info: z.object({
      firstName: z.string(),
      lastName: z.string(),
    }),
    email: z.string(),
  })
  .transform((user) => {
    return {
      id: user.uuid,
      value: user.info.firstName + ' ' + user.info.lastName,
    }
  })
```

```typescript
const mapData = (data: any) => twitterUserSchema.or(userSchema).or(googleUserSchema).parse(data)

export const users = [mapData(twitterUser), mapData(user), mapData(googleUser)]
```

[![Edit zod-mapper-object](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/jovial-worker-dl978e?fontsize=14&hidenavigation=1&theme=dark)

### Summary

You can add as many non-overlapping schemas as you need. Zod throws an error when none matches the input, while the result keeps a consistent type.

This approach is useful for mapping JavaScript data, but keep the mapper easy to follow. Too many schemas and types make an application harder to maintain, so add a union only when it reflects a real input source.

Zod can do much more, so explore the library when you need it. For a deeper look, see [this Zod tutorial](https://www.totaltypescript.com/tutorials/zod).

## Sitemap

See the full [sitemap](https://stijnvanhulle.be/sitemap.md) for all pages.
