Edited at

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