zodを使ってJSON.parseしたオブジェクトをvalidationして型安全にしたい

# やりたいこと

`JSON.parse(json)`して得られたオブジェクトをTypescriptの型でバリデーションしたい

# なぜやりたいか

APIなどからjson文字列を受け取る際、`JSON.parse(json)`のようにして文字列を解析してjavascriptのオブジェクトを構築しますが、この`JSON.parse`の返り値の型が`any`なので、変数に代入する型をいくら指定してもしれっと代入可能になってしまいます。
そのため、↓のようなコードはコンパイルは通ってもランタイムでエラーになってしまいます。

```typescript
type Post = {
  id: number
  title: string
  content: string
}

const post: Post = JSON.parse("{ id: 1, title: 'title' }") // JSON.parseの返り値がanyなので普通に代入できてしまう。

post.content.length // => contentはundefinedなのでerror
```
せっかくTypescriptで型のある開発を満喫していても、anyの前にはすべてが崩壊します。

Typescriptは実行時の型情報の提供は目指していないので、Proposalな仕様であるdecorator等を使ってアノテーションを記載するか、それ以外の方法を使って実行時のvalidationに対応する必要があります。

> Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals%EF%BC%89

# やったこと

validationライブラリであるzodを使って、JSON.parseしたオブジェクトをvalidationするサンプルコードを書きました。

https://github.com/tukamon5151/zod-test/blob/main/index.ts

# zodとは

typescript firstなvalidationライブラリです。類似のライブラリに[yup](https://github.com/jquense/yup)や[io-ts](https://github.com/gcanti/io-ts)などがありますが、Typescriptへの親和性や、重複するスキーマ・型宣言の排除を目標に様々な機能が設計されています。
使い方も非常にシンプルで、ファイルサイズもminifyされたファイルが8kbと非常に小さいのも良い点です。
また、Next.jsをラッパーしたフルスタックフレームワークで最近話題の[blitz.js](https://github.com/blitz-js/blitz)のvalidationライブラリとして採用されていたりと、なかなか注目のライブラリとなっています。

zodの仕様については[公式リポジトリ](https://github.com/colinhacks/zod)か、[こちらの記事](https://zenn.dev/uttk/articles/bd264fa884e026)が日本語で非常によくまとまっているので一度見てみてください。

# 実際のコード

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

const User = z.object({
    id: z.number(),
    name: z.string(),
    email: z.string().email()
})

type User = z.infer<typeof User>

const Post = z.object({
    id: z.number(),
    title: z.string(),
    user: User
})

type Post = z.infer<typeof Post>

const validJson = JSON.stringify({
  "id": 1,
  "title": 'title',
  "user": {
    "id": 1,
    "name": "name",
    "email": "email@email.com"
  }
})

const invalidJson = JSON.stringify({
  "id": 1,
  "title": 'title',
  "user": {
    "id": 1,
    // "name": "name", name should be required
    "email": "email@email.com"
  }
})

try {
  const validPost: Post = Post.parse(JSON.parse(validJson))
  console.log('validPost parse success')
  console.log(validPost)
  const invalidPost: Post = Post.parse(JSON.parse(invalidJson))
  console.log('invalidPost parse success') // not run
  console.log(invalidPost) // not run
} catch (e) {
  console.log(e)
  /**
  ZodError: [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "undefined",
      "path": [
        "user",
        "name"
      ],
      "message": "Required"
    }
  ]
  **/
}
```

まず、以下のコードでvalidation schemaを定義しています。

```typescript
const User = z.object({
    id: z.number(),
    name: z.string(),
    email: z.string().email()
})
```

上記はオブジェクト型のschemaを定義していますが、もちろんプリミティブ型や、Typescriptで定義されているUtilityTypesライクな `pick` `omit` `partial`など、他にも様々な便利な関数が定義されています。

ネストしたオブジェクトの型などももちろん柔軟に定義できます

```typescript
const Post = z.object({
    id: z.number(),
    title: z.string(),
    user: User // 先程作成したvalidation schema
})
```

続いて、先程定義したvalidation schemaから型を生成するコードです

```typescript
type Post = z.infer<typeof Post>
```

型定義とvalidation schemaの定義が1度に行えるので、重複管理にならなくて嬉しいですね。

そして、実際に検証を行うコードは以下で、
parseに渡された引数を検証し、問題なければ `Post`型としてリターンします。

```typescript
const validPost: Post = Post.parse(JSON.parse(validJson))
```

検証に引っかかった場合は`ZodError`オブジェクトがthrowされます

```typescript
try {
    const invalidPost = Post.parse(JSON.parse(invalidJson))
} catch (e) {
    console.log(e)
    /**
    ZodError: [
      {
        "code": "invalid_type",
        "expected": "string",
        "received": "undefined",
        "path": [
          "user",
          "name"
        ],
        "message": "Required"
      }
    ]
     **/
}
```

`parse`を使わず`safeParse`関数という使うと、throwされず、検証結果とデータ/エラーを返します。

```typescript
Post.safeParse(JSON.pase(validJson))
// => { success: true; data: { // ... } }

Post.safeParse(JSON.parse(invalidJson))
// => { success: false; error: ZodError }

```
https://github.com/colinhacks/zod#safe-parse

以上で、実現したかったJSON.parseしたオブジェクトをvalidationして型安全にするという目的は達成できました。
型があるっていいですね。

# 終わりに

以下の記事でも書かれているように、`ZodのSchemaと既存の型のダブルメンテ問題`など実運用をする上での細かい対応や注意するべき点があったり、最新のver3は現在betaだったりと、全員に進められるものというわけではありませんが、個人的にAPIのシンプルさや設計思想はとても気に入っているので、継続して使っていきたいなと思いました。

https://zenn.dev/ynakamura/articles/65d58863563fbc