Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

動機

 外界のデータに対して、どのように型付けを行うか - これは人類の当面の課題である。外界からアプリケーションに取り込んだデータに対して、内部で扱いやすいように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

kentrino
Ruby, Swift, Go
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away