💎 Zod 4 is now stable!  Read the announcement.
Zod logo

發布說明

經過一年的積極開發:Zod 4 現已穩定!它更快、更輕量、更 tsc 高效,並實現了一些長期要求的功能。

❤️

非常感謝 Clerk,他們通過极其慷慨的 OSS Fellowship 支持了我對 Zod 4 的工作。在整個(比預期長得多的!)開發過程中,他們都是了不起的合作夥伴。

版本控制

升級:

npm install zod@^4.0.0

有關重大變更的完整列表,請參閱 遷移指南。本文重點介紹新功能和增強功能。

為什麼要推出新的主要版本?

Zod v3.0 於 2021 年 5 月發布(!)。那時 Zod 在 GitHub 上有 2700 顆星,每週下載量為 60 萬次。今天,它擁有 3.78 萬顆星,每週下載量達到 3100 萬次(比 6 週前測試版發布時的 2300 萬次有所增加!)。在經歷了 24 個次要版本後,Zod 3 代碼庫已達到瓶頸;最常被要求的功能和改進需要進行重大變更。

Zod 4 一舉解決了 Zod 3 的許多長期存在的設計限制,為幾個長期要求的功能和性能的巨大飛躍鋪平了道路。它關閉了 Zod 10 個最高票數的未解決問題 中的 9 個。幸運的話,它將成為未來許多年新的基礎。

有關新功能的快速瀏覽,請參閱目錄。點擊任何項目即可跳轉到該部分。

基準測試

您可以在 Zod 存儲庫中親自運行這些基準測試:

$ git clone [email protected]:colinhacks/zod.git
$ cd zod
$ git switch v4
$ pnpm install

然後運行特定的基準測試:

$ pnpm bench <name>

14 倍更快的字串解析

$ pnpm bench string
runtime: node v22.13.0 (arm64-darwin)
 
benchmark      time (avg)             (min max)       p75       p99      p999
------------------------------------------------- -----------------------------
 z.string().parse
------------------------------------------------- -----------------------------
zod3          363 µs/iter       (338 µs 683 µs)    351 µs    467 µs    572 µs
zod4       24'674 ns/iter    (21'083 ns 235 µs) 24'209 ns 76'125 ns    120 µs
 
summary for z.string().parse
  zod4
   14.71x faster than zod3

7 倍更快的陣列解析

$ pnpm bench array
runtime: node v22.13.0 (arm64-darwin)
 
benchmark      time (avg)             (min max)       p75       p99      p999
------------------------------------------------- -----------------------------
 z.array() parsing
------------------------------------------------- -----------------------------
zod3          147 µs/iter       (137 µs 767 µs)    140 µs    246 µs    520 µs
zod4       19'817 ns/iter    (18'125 ns 436 µs) 19'125 ns 44'500 ns    137 µs
 
summary for z.array() parsing
  zod4
   7.43x faster than zod3

6.5 倍更快的物件解析

這運行了 Moltar 驗證庫基準測試

$ pnpm bench object-moltar
benchmark      time (avg)             (min max)       p75       p99      p999
------------------------------------------------- -----------------------------
 z.object() safeParse
------------------------------------------------- -----------------------------
zod3          805 µs/iter     (771 µs 2'802 µs)    804 µs    928 µs  2'802 µs
zod4          124 µs/iter     (118 µs 1'236 µs)    119 µs    231 µs    329 µs
 
summary for z.object() safeParse
  zod4
   6.5x faster than zod3

tsc 實例化減少 100 倍

考慮以下簡單的文件:

import * as z from "zod";
 
export const A = z.object({
  a: z.string(),
  b: z.string(),
  c: z.string(),
  d: z.string(),
  e: z.string(),
});
 
export const B = A.extend({
  f: z.string(),
  g: z.string(),
  h: z.string(),
});

使用 "zod/v3"tsc --extendedDiagnostics 編譯此文件會導致 >25000 次類型實例化。使用 "zod/v4" 僅導致約 175 次。

Zod 存儲庫包含一個 tsc 基準測試遊樂場。使用 packages/tsc 中的編譯器基準測試親自嘗試一下。具體數字可能會隨著實現的發展而變化。

$ cd packages/tsc
$ pnpm bench object-with-extend

更重要的是,Zod 4 重新設計並簡化了 ZodObject 和其他架構類的泛型,以避免一些有害的「實例化爆炸」。例如,重複鏈接 .extend().omit()——這在以前會導致編譯器問題:

import * as z from "zod";
 
export const a = z.object({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const b = a.omit({
  a: true,
  b: true,
  c: true,
});
 
export const c = b.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const d = c.omit({
  a: true,
  b: true,
  c: true,
});
 
export const e = d.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const f = e.omit({
  a: true,
  b: true,
  c: true,
});
 
export const g = f.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const h = g.omit({
  a: true,
  b: true,
  c: true,
});
 
export const i = h.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const j = i.omit({
  a: true,
  b: true,
  c: true,
});
 
export const k = j.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const l = k.omit({
  a: true,
  b: true,
  c: true,
});
 
export const m = l.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const n = m.omit({
  a: true,
  b: true,
  c: true,
});
 
export const o = n.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});
 
export const p = o.omit({
  a: true,
  b: true,
  c: true,
});
 
export const q = p.extend({
  a: z.string(),
  b: z.string(),
  c: z.string(),
});

在 Zod 3 中,這需要 4000ms 來編譯;並且添加對 .extend() 的額外調用會觸發「可能無限」錯誤。在 Zod 4 中,這在 400ms 內編譯完成,快了 10 倍

結合即將推出的 tsgo 編譯器,Zod 4 的編輯器性能將擴展到更大的架構和代碼庫。

核心包大小減少 2 倍

考慮以下簡單的腳本。

import * as z from "zod";
 
const schema = z.boolean();
 
schema.parse(true);

就驗證而言,這大約是最簡單的了。這是有意為之的;這是衡量 核心包大小 的好方法——即使在簡單的情況下也會最終進入包的代碼。我們將使用 rollup 以及 Zod 3 和 Zod 4 捆綁它,並比較最終的包。

PackageBundle (gzip)
Zod 312.47kb
Zod 45.36kb

Zod 4 的核心包小了約 57% (2.3 倍)。這很好!但我們可以做得更好。

介紹 Zod Mini

Zod 嚴重依賴方法的 API 從根本上來說很難進行 tree-shaking。即使我們簡單的 z.boolean() 腳本也會引入我們沒用到的一堆方法的實現,比如 .optional().array() 等。編寫更輕量的實現只能到此為止。這就是 Zod Mini 發揮作用的地方。

npm install zod@^4.0.0

這是一個帶有功能性、可 tree-shaking API 的 Zod 變體,與 zod 一一對應。Zod 使用方法的地方,Zod Mini 通常使用包裝函數:

import * as z from "zod/mini";
 
z.optional(z.string());
 
z.union([z.string(), z.number()]);
 
z.extend(z.object({ /* ... */ }), { age: z.number() });

並非所有方法都消失了!Zod 和 Zod Mini 中的解析方法是相同的:

import * as z from "zod/mini";
 
z.string().parse("asdf");
z.string().safeParse("asdf");
await z.string().parseAsync("asdf");
await z.string().safeParseAsync("asdf");

還有一個通用的 .check() 方法,用於添加細化(refinements)。

import * as z from "zod/mini";
 
z.array(z.number()).check(
  z.minLength(5), 
  z.maxLength(10),
  z.refine(arr => arr.includes(5))
);

以下頂級細化在 Zod Mini 中可用。它們對應於哪些 Zod 方法應該是不言自明的。

import * as z from "zod/mini";
 
// custom checks
z.refine();
 
// first-class checks
z.lt(value);
z.lte(value); // alias: z.maximum()
z.gt(value);
z.gte(value); // alias: z.minimum()
z.positive();
z.negative();
z.nonpositive();
z.nonnegative();
z.multipleOf(value);
z.maxSize(value);
z.minSize(value);
z.size(value);
z.maxLength(value);
z.minLength(value);
z.length(value);
z.regex(regex);
z.lowercase();
z.uppercase();
z.includes(value);
z.startsWith(value);
z.endsWith(value);
z.property(key, schema); // for object schemas; check `input[key]` against `schema`
z.mime(value); // for file schemas (see below)
 
// overwrites (these *do not* change the inferred type!)
z.overwrite(value => newValue);
z.normalize();
z.trim();
z.toLowerCase();
z.toUpperCase();

這種更加函數式的 API 使打包器更容易對您未使用的 API 進行 tree-shake。雖然對於大多數用例仍然推薦使用常規 Zod,但任何對包大小有嚴格限制的項目都應考慮 Zod Mini。

核心包大小減少 6.6 倍

這是上面的腳本,更新為使用 "zod/mini" 而不是 "zod"

import * as z from "zod/mini";
 
const schema = z.boolean();
schema.parse(false);

當我們使用 rollup 構建此代碼時,gzip 壓縮後的包大小為 1.88kb。與 zod@3 相比,核心包大小減少了 85% (6.6 倍)。

PackageBundle (gzip)
Zod 312.47kb
Zod 4 (regular)5.36kb
Zod 4 (mini)1.88kb

欲了解更多信息,請參閱專用的 zod/mini 文檔頁面。完整的 API 詳細信息混合在現有的文檔頁面中;只要 API 有分歧,代碼塊就會包含 "Zod""Zod Mini" 的單獨選項卡。

元數據 (Metadata)

Zod 4 引入了一個新系統,用於向您的架構添加強類型元數據。元數據不存儲在架構本身內部;相反,它存儲在將架構與某些類型化元數據相關聯的「架構註冊表」中。要使用 z.registry() 創建註冊表:

import * as z from "zod";
 
const myRegistry = z.registry<{ title: string; description: string }>();

將架構添加到您的註冊表:

const emailSchema = z.string().email();
 
myRegistry.add(emailSchema, { title: "Email address", description: "..." });
myRegistry.get(emailSchema);
// => { title: "Email address", ... }

或者,為了方便起見,您可以在架構上使用 .register() 方法:

emailSchema.register(myRegistry, { title: "Email address", description: "..." })
// => returns emailSchema

全局註冊表

Zod 還導出了一個全局註冊表 z.globalRegistry,它接受一些常見的兼容 JSON Schema 的元數據:

z.globalRegistry.add(z.string(), { 
  id: "email_address",
  title: "Email address",
  description: "Provide your email",
  examples: ["[email protected]"],
  extraKey: "Additional properties are also allowed"
});

.meta()

為了方便地將架構添加到 z.globalRegistry,請使用 .meta() 方法。

z.string().meta({ 
  id: "email_address",
  title: "Email address",
  description: "Provide your email",
  examples: ["[email protected]"],
  // ...
});

為了與 Zod 3 兼容,.describe() 仍然可用,但首選 .meta()

z.string().describe("An email address");
 
// equivalent to
z.string().meta({ description: "An email address" });

JSON Schema 轉換

Zod 4 通過 z.toJSONSchema() 引入了第一方 JSON Schema 轉換。

import * as z from "zod";
 
const mySchema = z.object({name: z.string(), points: z.number()});
 
z.toJSONSchema(mySchema);
// => {
//   type: "object",
//   properties: {
//     name: {type: "string"},
//     points: {type: "number"},
//   },
//   required: ["name", "points"],
// }

z.globalRegistry 中的任何元數據都會自動包含在 JSON Schema 輸出中。

const mySchema = z.object({
  firstName: z.string().describe("Your first name"),
  lastName: z.string().meta({ title: "last_name" }),
  age: z.number().meta({ examples: [12, 99] }),
});
 
z.toJSONSchema(mySchema);
// => {
//   type: 'object',
//   properties: {
//     firstName: { type: 'string', description: 'Your first name' },
//     lastName: { type: 'string', title: 'last_name' },
//     age: { type: 'number', examples: [ 12, 99 ] }
//   },
//   required: [ 'firstName', 'lastName', 'age' ]
// }

有關自定義生成的 JSON Schema 的信息,請參閱 JSON Schema 文檔

遞歸物件 (Recursive objects)

這是一個意想不到的功能。經過多年試圖解決這個問題,我終於 找到了一種方法 來在 Zod 中正確推斷遞歸物件類型。要定義遞歸類型:

const Category = z.object({
  name: z.string(),
  get subcategories(){
    return z.array(Category)
  }
});
 
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }

您還可以表示 相互遞歸類型

const User = z.object({
  email: z.email(),
  get posts(){
    return z.array(Post)
  }
});
 
const Post = z.object({
  title: z.string(),
  get author(){
    return User
  }
});

與 Zod 3 的遞歸類型模式不同,不需要類型轉換。結果架構是普通的 ZodObject 實例,並且可以使用全套方法。

Post.pick({ title: true })
Post.partial();
Post.extend({ publishDate: z.date() });

檔案架構 (File schemas)

要驗證 File 實例:

const fileSchema = z.file();
 
fileSchema.min(10_000); // minimum .size (bytes)
fileSchema.max(1_000_000); // maximum .size (bytes)
fileSchema.mime(["image/png"]); // MIME type

國際化 (Internationalization)

Zod 4 引入了一個新的 locales API,用於將錯誤訊息全局翻譯成不同的語言。

import * as z from "zod";
 
// configure English locale (default)
z.config(z.locales.en());

請參閱 自定義錯誤 中的完整支持語言環境列表;此部分將隨著受支持語言的增加而隨時更新。

錯誤美化打印 (Error pretty-printing)

zod-validation-error 包的流行表明,對於美化打印錯誤的官方 API 有巨大的需求。如果您目前正在使用該包,請務必繼續使用它。

Zod 現在實現了一個頂級 z.prettifyError 函數,用於將 ZodError 轉換為用戶友好的格式化字串。

const myError = new z.ZodError([
  {
    code: 'unrecognized_keys',
    keys: [ 'extraField' ],
    path: [],
    message: 'Unrecognized key: "extraField"'
  },
  {
    expected: 'string',
    code: 'invalid_type',
    path: [ 'username' ],
    message: 'Invalid input: expected string, received number'
  },
  {
    origin: 'number',
    code: 'too_small',
    minimum: 0,
    inclusive: true,
    path: [ 'favoriteNumbers', 1 ],
    message: 'Too small: expected number to be >=0'
  }
]);
 
z.prettifyError(myError);

這將返回以下可美化打印的多行字串:

✖ Unrecognized key: "extraField"
✖ Invalid input: expected string, received number
  → at username
✖ Invalid input: expected number, received string
  → at favoriteNumbers[1]

目前格式不可配置;這可能會在未來發生變化。

頂級字串格式 (Top-level string formats)

所有「字串格式」(電子郵件等)都已升級為 z 模組上的頂級函數。這既更簡潔又更易於 tree-shake。方法等效項(z.string().email() 等)仍然可用,但已被棄用。它們將在下一個主要版本中被移除。

z.email();
z.uuidv4();
z.uuidv7();
z.uuidv8();
z.ipv4();
z.ipv6();
z.cidrv4();
z.cidrv6();
z.url();
z.e164();
z.base64();
z.base64url();
z.jwt();
z.lowercase();
z.iso.date();
z.iso.datetime();
z.iso.duration();
z.iso.time();

自定義電子郵件正則表達式

z.email() API 現在支持自定義正則表達式。沒有一個標準的電子郵件正則表達式;不同的應用程序可能會選擇更嚴格或更寬鬆。為了方便起見,Zod 導出了一些常見的正則表達式。

// Zod's default email regex (Gmail rules)
// see colinhacks.com/essays/reasonable-email-regex
z.email(); // z.regexes.email
 
// the regex used by browsers to validate input[type=email] fields
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email
z.email({ pattern: z.regexes.html5Email });
 
// the classic emailregex.com regex (RFC 5322)
z.email({ pattern: z.regexes.rfc5322Email });
 
// a loose regex that allows Unicode (good for intl emails)
z.email({ pattern: z.regexes.unicodeEmail });

模板字串類型 (Template literal types)

Zod 4 實現了 z.templateLiteral()。模板字串類型可能是 TypeScript 類型系統中以前無法表示的最大功能。

const hello = z.templateLiteral(["hello, ", z.string()]);
// `hello, ${string}`
 
const cssUnits = z.enum(["px", "em", "rem", "%"]);
const css = z.templateLiteral([z.number(), cssUnits]);
// `${number}px` | `${number}em` | `${number}rem` | `${number}%`
 
const email = z.templateLiteral([
  z.string().min(1),
  "@",
  z.string().max(64),
]);
// `${string}@${string}` (the min/max refinements are enforced!)

每個可以被字串化的 Zod 架構類型都存儲一個內部正則表達式:字串、字串格式如 z.email()、數字、布爾值、bigint、枚舉、字面量、undefined/optional、null/nullable 和其他模板字串。z.templateLiteral 構造函數將這些連接成一個超級正則表達式,因此像字串格式(z.email())這樣的東西會被正確強制執行(但自定義細化不會!)。

如需更多信息,請閱讀 模板字串文檔

數字格式 (Number formats)

添加了新的數字「格式」用於表示固定寬度的整數和浮點類型。這些返回一個 ZodNumber 實例,其中已添加了適當的最小值/最大值約束。

z.int();      // [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
z.float32();  // [-3.4028234663852886e38, 3.4028234663852886e38]
z.float64();  // [-1.7976931348623157e308, 1.7976931348623157e308]
z.int32();    // [-2147483648, 2147483647]
z.uint32();   // [0, 4294967295]

同樣,也添加了以下 bigint 數字格式。這些整數類型超出了 JavaScript 中 number 可以安全表示的範圍,因此這些返回一個 ZodBigInt 實例,其中已添加了適當的最小值/最大值約束。

z.int64();    // [-9223372036854775808n, 9223372036854775807n]
z.uint64();   // [0n, 18446744073709551615n]

布爾字串 (Stringbool)

現有的 z.coerce.boolean() API 非常簡單:假值(falseundefinednull0""NaN 等)變為 false,真值變為 true

這仍然是一個很好的 API,其行為與其他 z.coerce API 一致。但是一些用戶要求更複雜的「環境變數風格」布爾值強制轉換。為了支持這一點,Zod 4 引入了 z.stringbool()

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
 
strbool.parse(/* anything else */); // ZodError<[{ code: "invalid_value" }]>

自定義真值和假值:

z.stringbool({
  truthy: ["yes", "true"],
  falsy: ["no", "false"]
})

有關更多信息,請參閱 z.stringbool() 文檔

簡化的錯誤自定義 (Simplified error customization)

Zod 4 中的大多數重大變更都涉及 錯誤自定義 API。在 Zod 3 中,它們有些混亂;Zod 4 使事情變得更加優雅,我認為值得在此強調。

長話短說,現在有一個統一的 error 參數用於自定義錯誤,替換了以下 APIs:

error 替換 message。(message 參數仍然受支持,但已棄用。)

- z.string().min(5, { message: "Too short." });
+ z.string().min(5, { error: "Too short." });

error(函數語法)替換 invalid_type_errorrequired_error

// Zod 3
- z.string({ 
-   required_error: "This field is required" 
-   invalid_type_error: "Not a string", 
- });
 
// Zod 4 
+ z.string({ error: (issue) => issue.input === undefined ? 
+  "This field is required" :
+  "Not a string" 
+ });

error(函數語法)替換 errorMap

// Zod 3 
- z.string({
-   errorMap: (issue, ctx) => {
-     if (issue.code === "too_small") {
-       return { message: `Value must be >${issue.minimum}` };
-     }
-     return { message: ctx.defaultError };
-   },
- });
 
// Zod 4
+ z.string({
+   error: (issue) => {
+     if (issue.code === "too_small") {
+       return `Value must be >${issue.minimum}`
+     }
+   },
+ });

升級的 z.discriminatedUnion() (Upgraded z.discriminatedUnion())

判別聯合(Discriminated unions)現在支持许多以前不支持的架構類型,包括聯合和管道:

const MyResult = z.discriminatedUnion("status", [
  // simple literal
  z.object({ status: z.literal("aaa"), data: z.string() }),
  // union discriminator
  z.object({ status: z.union([z.literal("bbb"), z.literal("ccc")]) }),
  // pipe discriminator
  z.object({ status: z.literal("fail").transform(val => val.toUpperCase()) }),
]);

也許最重要的是,判別聯合現在可以 組合——您可以將一個判別聯合用作另一個判別聯合的成員。

const BaseError = z.object({ status: z.literal("failed"), message: z.string() });
 
const MyResult = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.discriminatedUnion("code", [
    BaseError.extend({ code: z.literal(400) }),
    BaseError.extend({ code: z.literal(401) }),
    BaseError.extend({ code: z.literal(500) })
  ])
]);

z.literal() 中的多個值

z.literal() API 現在可選地支持多個值。

const httpCodes = z.literal([ 200, 201, 202, 204, 206, 207, 208, 226 ]);
 
// previously in Zod 3:
const httpCodes = z.union([
  z.literal(200),
  z.literal(201),
  z.literal(202),
  z.literal(204),
  z.literal(206),
  z.literal(207),
  z.literal(208),
  z.literal(226)
]);

細化存在於架構內部 (Refinements live inside schemas)

在 Zod 3 中,它們存儲在包裝原始架構的 ZodEffects 類中。這很不方便,因為這意味著您無法將 .refine() 與其他架構方法(如 .min())交錯使用。

z.string()
  .refine(val => val.includes("@"))
  .min(5);
// ^ ❌ Property 'min' does not exist on type ZodEffects<ZodString, string, string>

在 Zod 4 中,細化存儲在架構本身內部,因此上面的代碼可以按預期工作。

z.string()
  .refine(val => val.includes("@"))
  .min(5); // ✅

.overwrite()

.transform() 方法非常有用,但它有一個主要缺點:輸出類型在運行時不再是 可內省的。轉換函數是一個可以返回任何東西的黑盒。這意味著(除其他外)沒有可靠的方法可以將架構轉換為 JSON Schema。

const Squared = z.number().transform(val => val ** 2);
// => ZodPipe<ZodNumber, ZodTransform>

Zod 4 引入了一個新的 .overwrite() 方法,用於表示 不改變推斷類型 的轉換。與 .transform() 不同,此方法返回原始類的實例。覆蓋函數存儲為細化,因此它不會(也不能)修改推斷類型。

z.number().overwrite(val => val ** 2).max(100);
// => ZodNumber

現有的 .trim().toLowerCase().toUpperCase() 方法已使用 .overwrite() 重新實現。

可擴展的基礎:zod/v4/core (An extensible foundation: zod/v4/core)

雖然這對大多數 Zod 用戶來說並不重要,但值得強調。Zod Mini 的添加需要創建一個共享子包 zod/v4/core,其中包含 Zod 和 Zod Mini 之間共享的核心功能。

起初我很抗拒這一點,但現在我認為它是 Zod 4 最重要的功能之一。它讓 Zod 從一個簡單的庫升級為一個快速驗證「基底」,可以灑入其他庫中。

如果您正在構建架構庫,請參考 Zod 和 Zod Mini 的實現,了解如何在 zod/v4/core 提供的基礎上進行構建。如有幫助或反饋,請隨時在 GitHub 討論區或通過 X/Bluesky 聯繫。

總結 (Wrapping up)

我正計劃寫一系列額外的文章,解釋像 Zod Mini 這樣的主要功能背後的設計過程。隨著這些文章的發布,我將更新此部分。

對於庫作者,現在有一個專用的 給庫作者 指南,描述了在 Zod 之上構建的最佳實踐。它回答了有關如何同時支持 Zod 3 和 Zod 4(包括 Mini)的常見問題。

pnpm upgrade zod@latest

快樂解析!
— Colin McDonnell @colinhacks