JavaScript
Node.js
TypeScript
jsonschema
QuickType

QuicktypeでJSON Schemaを簡単に生成し、型安全な最高の開発体験を得た話

動機

 外界のデータに対して、どのように型付けを行うか - これは人類の当面の課題である。外界からアプリケーションに取り込んだデータに対して、内部で扱いやすいようにnon-nullの型を書くと、予期しないクラッシュを引き起こしてしまった、というような経験を誰しもお持ちではないだろうか。一方で、外界の状況と一致した型を書くと、冗長にnullチェックを書く羽目になり、デベロッパー・エクスペリエンスがよろしくない。このジレンマから逃れるために、外界からデータを取り込む境界部分で包括的なアサーションを施して、アプリケーション内部では対応する型をもっているものとして扱いたい。
 
 JavaScriptでJSON Schemaを用いてアサーションを行うライブラリは知っていたので、うまい具合にJSON Schemaとコードを同期させるソリューションがあれば、JSON Schemaとアプリケーションのコードを同時にメンテする手間も省けて良い。できれば、TypeScriptの簡潔な型表現から、読みにくく書きづらいJSON Schemaを自動生成してくれるアプローチがあればなお良い。以上の観点で、簡単にリサーチしてみた。

TypeScriptの場合、いろいろ検討(読まなくても良い)

ajv

https://github.com/epoberezkin/ajv
とりあえず、こいつにバリデーションをやらせたい。だが、JSON Schemaは書きたくない。

Joi

https://github.com/hapijs/joi
こいつも検討したが、過去の遺物っぽい。

typescript-json-schema

https://github.com/YousefED/typescript-json-schema
TypeScriptからJSON Schemaを生成してくれるらしい。なんか動かないし、ドキュメントがいまいち。スターも350程度とイマイチなので、初手で動かない時点で諦めた。

json-schma-to-typescript

https://github.com/bcherny/json-schema-to-typescript
こちらはJSON Schemaから生成するやつ。JSON Schema絶対書きたくない。

jsonschema.net

https://jsonschema.net/
JSONを書けばJSON Schemaを得られるが、JSON Schemaをコミットしてしまうとメンテナビリティが下がるので、何かで生成したい。

typson

https://github.com/lbovet/typson
こいつもTypeScriptからJSON Schemaを生成してくれるらしいが、ドキュメントがいまいち。

Swagger, GraphQLとか

ちょっと目的と違うっぽい

QuickType

ひととおり調べてみて、これを見つけた。
https://github.com/quicktype/quicktype
npm install -g quicktypeから10秒、JSON Schemaが完成した。

詳細

https://quicktype.io/
コンセプトとしては、JSON Schemaから各種コード(Swift, Go, Rust, Kotlin, Ruby等に対応)を生成できるというものらしいが、TypeScriptに限り逆もできる。Swaggerとの立ち位置の違いはそこまで分かっていないものの、Quicktypeの方はAPIに含まれるオブジェクトのみにフォーカスしているようで、何をやってくれるツールなのかすぐに分かる上に、他の優れたツールとの連携も良い(cf. http://www.azquotes.com/author/50799-Douglas_McIlroy )。 TypeScriptで書かれている。開発開始からまだ1年も経っていない。スター数は1046(2018年6月22日時点)

Webコンソールもある (https://app.quicktype.io/ )

TypeScriptの型はこちら

type.ts
export type PushEntry = {
  file: string,
  bucket: string,
  sha1: string,
  repo: string,
  tags: {[key: string]: string},
}

export type PushFile = {
  files: PushEntry[]
}

こちらが生成コマンド

TypeScriptのソースからschemaを作るのはExperimentalらしいがきちんと動く。作ったschemaをもう一回TypeScriptのコードに戻すと、アサーションをやってくれるっぽいコードも出力される。ajvが不要な可能性もあるが今回は詳細なリサーチを省略。

$ quicktype type.ts -o schema.json --lang schema
schema.json
{
    "$schema": "http://json-schema.org/draft-06/schema#",
    "definitions": {
        "PushEntry": {
            "type": "object",
            "properties": {
                "file": {
                    "type": "string",
                    "title": "file"
                },
                "bucket": {
                    "type": "string",
                    "title": "bucket"
                },
                "sha1": {
                    "type": "string",
                    "title": "sha1"
                },
                "repo": {
                    "type": "string",
                    "title": "repo"
                },
                "tags": {
                    "type": "object",
                    "additionalProperties": {
                        "type": "string"
                    },
                    "title": "tags"
                }
            },
            "required": [
                "bucket",
                "file",
                "repo",
                "sha1",
                "tags"
            ]
        },
        "PushFile": {
            "type": "object",
            "properties": {
                "files": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "file": {
                                "type": "string",
                                "title": "file"
                            },
                            "bucket": {
                                "type": "string",
                                "title": "bucket"
                            },
                            "sha1": {
                                "type": "string",
                                "title": "sha1"
                            },
                            "repo": {
                                "type": "string",
                                "title": "repo"
                            },
                            "tags": {
                                "type": "object",
                                "additionalProperties": {
                                    "type": "string"
                                },
                                "title": "tags"
                            }
                        },
                        "required": [
                            "bucket",
                            "file",
                            "repo",
                            "sha1",
                            "tags"
                        ]
                    },
                    "title": "files"
                }
            },
            "required": [
                "files"
            ]
        }
    }
}

バリデーションを行う

import * as Ajv from "ajv"

function compliedValidator(): Ajv.ValidateFunction {
  const ajv = new Ajv
  // 先程生成したやつ
  const schema: any = require("./schema.json")
  const validate = ajv.compile(schema.definitions.PushEntry)
  return validate
}

function successExample(validate: Ajv.ValidateFunction) {
  const validation = validate({
    file: "hoge",
    bucket: "fuga",
    sha1: "moge",
    repo: "muga",
    tags: {}
  })
  console.log(validation)
  console.log(validate.errors)
}

function failExample(validate: Ajv.ValidateFunction) {
  const validation = validate({
    file: "hoge",
    sha1: "moge",
    repo: "muga",
    tags: {}
  })
  console.log(validation)
  console.log(validate.errors)
}

const validator = compliedValidator()
successExample(validator)
failExample(validator)

出力

目的達成。ajvによるバリデーションを通ったあと、アプリケーション内部では、指定した型に従っているものとして扱って良い。

true
null
false
[ { keyword: 'required',
    dataPath: '',
    schemaPath: '#/required',
    params: { missingProperty: 'bucket' },
    message: 'should have required property \'bucket\'' } ]

ふりかえり

今回は、とにかく簡単にやるのが目的だったので、TypeScriptから生成した。アプリケーションが育ち、開発者が増えるにつれて、Quicktypeが想定したユースケースに従い、JSON Schemaの方を中心に据えて開発を進めるのが良い気がする。

苦情・まさかりはコチラ

twitter: _kentrino