🔄

JSON SchemaをValibotに変換するライブラリを自作した

に公開

Claude CodeによるVibe Codingの練習にこんなものを書いてみました。

https://github.com/hayatosc/json-schema-to-valibot

https://www.npmjs.com/package/json-schema-to-valibot

その名の通り、JSON Schemaを変換してValibotのスキーマに変換するライブラリです。 json-schema-to-zod というライブラリのValibot版がなかったので作りました。

基本の使い方

こんな感じの schema.json を作って、

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "email": {
      "type": "string", 
      "format": "email"
    },
    "age": {
      "type": "integer",
      "minimum": 0,
      "maximum": 150
    },
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "uniqueItems": true
    }
  },
  "required": ["name", "email"]
}

json-schema-to-valibot -i schema.json -o schema.ts のようにすれば schema.ts にValibotのスキーマが出力されます。

import * as v from 'valibot'

export const schema = v.object({
  "name": v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
  "age": v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(150))),
  "email": v.pipe(v.string(), v.email()),
  "tags": v.optional(v.pipe(v.array(v.string()), v.custom((input) => Array.isArray(input) && new Set(input).size === input.length, "Items must be unique")))
})

tags はユニークなアイテムのリストを表しているので v.custom() による特別な処理が入っています。

Claude Codeが強すぎる

Claude Codeに json-schema-to-zod を見てもらいながら書いてもらったのですが、ものの1時間程度で基本の部分は出来上がり、その後テストもあっという間に通過していたので驚きました。

私はClaude Pro(20ドル)しか契約していない(つまり最上位のOpus 4は使ってない)のにも関わらずこのクオリティのものを出してくるとは思いもしませんでしたし、これよりも優秀と聞くOpus 4はとんでもないんだろうなぁと。もう自分が1からコード書く時代は本当に終わりを迎えそう。

あまりに呆気なく終わってしまったので、追加でいくつか取り組んでみることにしました。

公式のテストスイートをやってみる

JSON Schemaが出している公式のテストスイートがあります。

https://github.com/json-schema-org/JSON-Schema-Test-Suite

これをsubmoduleとして置いて、テストを走らせて見ることにしました。 test-suite-runner.ts にそれが書いてあります。中身としては、Valibotのスキーマを生成して、それを使ってパースできるか試すみたいな感じです。

https://github.com/hayatosc/json-schema-to-valibot/blob/main/script/test-suite-runner.ts

執筆時点(v0.3.0)では全体で78.1%とそこそこの割合で合格しています。

failやerrorについては具体的に分析しながらどうやれば解決するのかを考えるように指示を出してみています。とはいえ通過率100%にすることが目的ではなく、あくまで良いValibotのコードを生成することが目的なので、極端にValibot本体から逸脱したコードは弾かなければいけません。この感覚をAIに伝えるのはなかなか難しいですね。

$ref によるモジュール分割に対応した

JSON Schemaは $ref を使って他の定義を参照できます。

{
  "type": "object",
  "properties": {
    "user": { "$ref": "#/definitions/User" }
  },
  "definitions": {
    "User": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "age": { "type": "number" }
      },
      "required": ["name"]
    }
  }
}

これを別々に分割して出力するようにしました。

import * as v from 'valibot'

export const User = v.object({
  "name": v.string(),
  "age": v.optional(v.number())
});

export const schema = v.object({
  "user": v.optional(User)
})

これで User の型だけ欲しいというケースにも v.infer<User> で対応できます。

循環参照に対応する

$ref に対応すると同時に循環参照に対応する必要が出てきます。

例えば以下のケースです。

{
  "type": "object",
  "properties": {
    "node": { "$ref": "#/definitions/Node" }
  },
  "definitions": {
    "Node": {
      "type": "object",
      "properties": {
        "value": { "type": "string" },
        "child": { "$ref": "#/definitions/Node" }
      }
    }
  }
}

このように、 Node の中で Node を参照するような場合ですね。Valibotでも v.lazy() を使うことで対応できるんですが、型情報が抜けてしまう問題があります。

というわけで、簡単な型定義も出力することにしてみました。

import * as v from 'valibot'

export type Node = { value?: string; child?: Node };

export const NodeSchema: v.GenericSchema<Node> = v.object({
  "value": v.optional(v.string()),
  "child": v.optional(v.lazy(() => NodeSchema))
});

export const schema = v.object({
  "node": v.optional(NodeSchema)
})

これで正しく型推論が行えるようになるはずですが、正直スキーマが大きくなったら失敗しそうなのでまだまだ荒削りって感じですね。

おわりに

https://www.npmjs.com/package/json-schema-to-valibot

というわけで、よければ使ってください。何か問題があれば報告してもらえると助かります。

Discussion