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