旧ブログ(ISSEN)から移行しました

【zod】v4で使えるようになったオススメのスキーマ

【zod】v4で使えるようになったオススメのスキーマ

西原月熙
西原月熙6分で読めます

はじめに

こんにちは、株式会社TOKOSのツキヤです!
2025年の7月に、zodのv4がリリースされましたね🚀
書き方が変わったり、便利なスキーマが追加されたりしました✨

Release notes | Zod

Zod 4 release notes and new features including performance improvements and breaking changes

zod.dev

そこで、すでにあるが書き方等が変わったスキーマと、新規で便利そうなスキーマ達を紹介しようと思います💪

v3からのマイグレーションガイドも出ているので、一旦バージョンアップだけしたい方はそちらを参考にしてみてください!

Migration guide | Zod

Complete changelog and migration guide for upgrading from Zod 3 to Zod 4

zod.dev

オススメのスキーマ

まずは、v3以前から有ったスキーマ達です!

z.int()

皆さん、numberがほしいときにフワッとz.number()にしていませんか?
z.number()だと、少数等も含まれてしまいます😨
明確に「整数」が良い時はz.int()を使いましょう!

import { z } from "zod"
const schema = z.int()
schema.parse(1) // 成功!
schema.parse(0.1) // エラー!

こちらはv3から有りましたが、z.number().int()と書く必要がありました。
v4からはnumber()の宣言ナシにそのままz.int()と呼べるようになったので覚えておきましょう💪

また、「整数かつ正の数」が良い場合は、z.int().positive()とも書けます!

import { z } from "zod"
const schema = z.int().positive()
schema.parse(1) // 成功!
schema.parse(0.1) // エラー!
schema.parse(-1) // エラー!

z.email()

メールアドレスのバリデーションを正規表現で頑張って設定してたりしてないですか??
そんな時に使えるのがz.email()です!

こちらは、一般的なメールアドレスの正規表現をカバーしてくれています✨

z.email()についても、v3からありました。ですが、こちらもz.string().email()string()が不要になっています。
また、カスタムもできるようになりました!!
細かく調整したい方はこちらも参照してください。

import { z } from "zod"
/*
 * Zodのデフォルトemailバリデーション(Gmail規則)
 * colinhacks.com/essays/reasonable-email-regex を参照
 */
z.email()
/*
 * ブラウザがinput[type=email]フィールドの検証に使用する正規表現
 * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email
 */
z.email({ pattern: z.regexes.html5Email })
 
// RFC 5322に準拠したクラシックなemailregex.comの正規表現
z.email({ pattern: z.regexes.rfc5322Email })
 
// Unicodeを許可する緩い正規表現(国際化メール対応)
z.email({ pattern: z.regexes.unicodeEmail })
ツキヤツキヤ

他のz.string()系と併用していたスキーマも、同じくstring()が不要になっているので合わせて覚えておきたいです!

extend()

こちらも元からあるもので、スキーマ同士を合成させるような働きでした。
ですが、merge()との違いがよく分かってませんでした🤔

import { z } from "zod"
 
const humanSchema = z.object({
  name: z.string().min(1),
  age: z.number().int().positive(),
})
 
const extendProgramerSchema = humanSchema.extend({
  occupation: z.string().min(1),
})
 
const mergeProgramerSchema = humanSchema.merge(
  z.object({
    occupation: z.string().min(1),
  }),
)
 
type Human = z.infer<typeof humanSchema>
// => { name: string; age: number }
 
type Programer = z.infer<typeof extendProgramerSchema>
// => { name: string; age: number; occupation: string } 🤔
 
type Programer = z.infer<typeof mergeProgramerSchema>
// => { name: string; age: number; occupation: string } 🤔

そういった背景もあってか、v4からはmerge()の方が非推奨になりました!
これで自信を持ってextend()を使えるので、皆さん覚えておきましょう🥳

...ただ、スキーマの合成の際のベストプラクティスは「shape + スプレッド構文」になったようです!
スプレッド構文を用いた方法が1番パフォーマンスが良いらしいので、僕は極力こちらを使うようにします!

import { z } from "zod"
 
const humanSchema = z.object({
  name: z.string().min(1),
  age: z.number().int().positive(),
})
 
const occupationSchema = z.object({
  occupation: z.string().min(1),
})
 
const programerSchema = z.object({
  ...humanSchema.shape,
  ...occupationSchema.shape,
})
 
type Programer = z.infer<typeof programerSchema>
// => { name: string; age: number; occupation: string } 🥳

refine()

こちらもv3から存在します!
皆さん1回は使ったことがあると思っていて、カスタムのバリデーションを書けるやつですね!

import { z } from "zod"
 
z.string().refine((val) => val.includes("@")) // 「@」が含まれていないとバリデーションエラー!

ただ、v3まではrefine()後にmin()等のスキーマを追加できませんでした🥲

import { z } from "zod"
 
z.string()
  .refine((val) => val.includes("@"))
  .min(10) // 型エラー🥲

ですが、v4からは可能になりました✨

import { z } from "zod"
 
z.string()
  .refine((val) => val.includes("@"))
  .min(10) // 型エラーにならない🥳

次からは新しいスキーマの紹介です!!

z.templateLiteral

説明するよりコードを読んだ方が分かりやすいので記載します!

import { z } from "zod"
 
const email = z.templateLiteral([z.string().min(1), "@", z.string().max(64)])
type Email = z.infer<typeof email>
// => `${string}@${string}`というテンプレートリテラル型になる✨

配列の要素をまとめた上でテンプレートリテラル型になってくれる感じです!
パッといい感じの具体例は思い浮かびませんが、覚えておくとどこかしらで使えそうです💪
(上記の例は、z.email()で代用できそうなので...)

z.stringbool()

文字列からいい感じにbooleanに変換したい時ありますよね?
例えば、文字列の"true"をbooleanのtrueにしたい時とかです!
そんな時に使えるのがこちらのstringbool()です!

import { z } from "zod"
 
const strbool = z.stringbool()
 
strbool.parse("true") // => true
strbool.parse("1") // => true
strbool.parse("yes") // => true
strbool.parse("on") // => true
strbool.parse("y") // => true
strbool.parse("enabled") // => true
 
strbool.parse("false") // => false
strbool.parse("0") // => false
strbool.parse("no") // => false
strbool.parse("off") // => false
strbool.parse("n") // => false
strbool.parse("disabled") // => false

こんな感じで、trueっぽい文字列はtrueに、falseっぽい文字列はfalseになります!

ただ、「自分で決めたいぜ!」って人もいると思います。
そんな人のためにも、自分で定義することが可能になっています。

import { z } from "zod"
 
const strbool = z.stringbool({
  truthy: ["yes", "true", "hoge"],
  falsy: ["no", "false", "huga"],
})
 
strbool.parse("yes") // => true
strbool.parse("true") // => true
strbool.parse("hoge") // => true
 
strbool.parse("no") // => false
strbool.parse("false") // => false
strbool.parse("huga") // => false

おわりに

今回は、v4になったzodの紹介をしました!
破壊的な変更も多く、この他にも便利になったり新しい機能等がいっぱい有るので、ぜひご自身でも確認してみてください!

この記事を書いた人

西原月熙
西原月熙

TOKOSのテックリード。上流工程からコーディング、インフラ系まで色々やっている器用貧乏です。最近は特にフロントエンドのキャッチアップに力を入れています💪 好きな音楽はボーカロイドです🤖