openapi-zod-clientが整数型のenumをZod公式の方針通りに変換してくれなかったりしたので諸々直してもらった
openapi-zod-client はOpenAPI 3.0+のYAML/JSONから Zod というか Zodios の定義を生成してくれるCLIツール。Playground もある。この記事で扱うバージョンは 1.4.5
。
これに手元のOASを通すと整数型のenumが――
- in: query
name: foo
schema:
type: integer
enum:
- 1
- 2
こうなってしまう。
{
name: "foo",
type: "Query",
schema: z.union([z.literal("1"), z.literal("2")]).int()
}
ついでに小数だとこうなる。
- in: query
name: bar
schema:
type: number
enum:
- 1.234
- 2.345
{
name: "bar",
type: "Query",
schema: z.union([z.literal("1.234"), z.literal("2.345")])
}
Zod公式としては整数型のenumは z.literal()
の z.union()
にしろ、ということらしい。
z.literal()
は数値のまま入れられる仕様なので文字列にされると困るし、 z.union()
に対して .int()
されても困る。
その場しのぎなら出力結果を正規表現で整形する対症療法でもなんとかなりそうではある。
targetString.replace(
/(z.union\(\[(?:z\.literal\("-?[0-9]+\.?[0-9]*"\)(?:, )?){1,}\]\))(?:\.int\(\))?/g,
(_m, p1) => p1.replaceAll('"', "")
)
openapi-zod-client側で対処しようとすると、ざっと見た感じ該当するのはこの辺っぽい。
まず schema
に enum
がぶら下がっていたら int()
は付与しないことにする。
if (schema.type === "integer" && !schema.enum) {
validations.push("int()");
}
変換してみる。
- in: query
name: foo
schema:
type: integer
enum:
- 1
- 2
{
name: "foo",
type: "Query",
schema: z.union([z.literal("1"), z.literal("2")])
}
次に type
が integer
または number
だったらそのまま z.literal()
に入れることにする。
null
の扱いは元々あったロジックを踏襲しておく。
if (schema.enum) {
if (schema.type === "string") {
// 略
}
+ if (schema.type === "integer" || schema.type === "number") {
+ return code.assign(
+ `z.union([${schema.enum
+ // eslint-disable-next-line sonarjs/no-nested-template-literals
+ .map((value) => `z.literal(${value === null ? "null" : `${value}`})`)
+ .join(", ")}])`
+ );
+ }
return code.assign(
// 略
);
}
変換してみる。
- in: query
name: foo
schema:
type: integer
enum:
- 1
- 2
- in: query
name: bar
schema:
type: number
enum:
- 1.234
- 234.5
{
name: "foo",
type: "Query",
schema: z.union([z.literal("1"), z.literal("2")])
},
{
name: "bar",
type: "Query",
schema: z.union([z.literal("1.234"), z.literal("234.5")])
}
テストをかけたらVitestの toMatchInlineSnapshot
が引っかかったので integer
と number
がそれぞれテストされるように変更。
expect(getSchemaAsZodString({ type: "integer", enum: [1, 2, 3, null] })).toMatchInlineSnapshot();
expect(getSchemaAsZodString({ type: "number", enum: [1.2, 3.4, 56.789, null] })).toMatchInlineSnapshot();
もう1回実行して内容を確認。
nullable
な件はこの辺が根拠になる模様。
Elements in the array might be of any type, including null.
って書いてあるし、これはissue案件かも?
issueを投げてみた。
一瞬で修正されパッチリリースされた。はやい。
本題から外れるけど、ちょっと実装精度が怪しくて現時点での実戦投入は厳しそう。
- patternに実体参照の制御文字を記述すると復号されたまま戻らない
- minLengthとmaxLengthを両方指定するとminLengthの固定長として処理されてしまう
- allOfに$refが3個以上あるとうまく連結されない
これらもissueで報告した。
#49で盛り上がった結果、自分の用途には十分な状態まで仕上がってしまった。
これで6000余行(絶賛増加中)のYAMLを変換してやれば、自前のフォームコンポーネントを使ったreact-hook-formとZodの繋ぎ込み作業を効率化できる……はず。