TypeScript のソースコードから JSON のバリデーションをするツール


TypeScript のインターフェイスやクラスの定義を使ってJSONのバリデーションをしたいという場合のため、TypedGateというツールを作りました。
このツールでは JSON Schema などのスキーマを別途定義することなく、TypeScriptソースコードそのもので型をバリデーションすることを目的としています。

例えば、以下のようなTypeScriptの定義がある場合、

export type Gender = 'female' | 'male' | 'other';

// ↓ これが ControlComment です
//   この場合 JSON の people プロパティを指しています
// @TG:path .people
export interface People {
  name: string;
  age: number;
  gender: Gender;
  isProgrammer: boolean;
}

上記のように // @TG:path .people という "ControlComment" をインターフェイスの直前に書くことで、JSONの該当する場所と比較して、バリデーションをします。この場合、以下のJSONは Valid となります。

{
  "people": {
    "name": "Linus Torvalds",
    "age": 49,
    "gender": "male",
    "isProgrammer": true
  }
}

動作の仕組みとしては、インターフェイスの定義の直前の ControlComment に // @TG:path .people と書いてあるため、 JSONの people というプロパティの

{
    "name": "Linus Torvalds",
    "age": 49,
    "gender": "male",
    "isProgrammer": true
}

の部分が interface People と比較されたのです。

正しくないJSONの場合

下記の画像の左がJSON、右がインターフェイスの定義とすると、JSONには available の定義がないためこのJSONは Not Valid であると出力されます。

右記 interface HomeConfig の ControlComment では左記JSONの .home というプロパティと比較せよ、と指示しており、TypedGate は JSON の home プロパティの部分と比較しますが、JSON には .home.contents[].available プロパティが欠如しているため、この JSON は Not Valid になるのです。

また、エラーが発生したソースファイルの場所とJSONのパスが表示されます。

上記の例では、HomeContent の定義の中で HomeContentName やら LatestSchedumeMeta や他複数の型を参照していることがわかると思います。TypedGateはこれらの定義を全て辿るため、 ControlComment はルートの部分だけに書けば動きます(この場合はHomeConfigに書く)。

使い方

このツールはコマンドラインツールとなっており、動作させるには tsconfig.json へのパス(--tsconfig)とソースのファイル名(--src)、JSONのファイルパス(--json)を引数に指定する必要があります。

-v を指定すると詳細なデバッグログを出力します。

インストールなしで使う

以下のコマンドを実行すると、npx の機能によりお手持ちのPCにインストールすることなくオンザフライで動作できます。

npx typedgate --tsconfig ./tsconfig.json --src ./src/index.ts --json ./some-json-file.json

インストールする

もしインストールをしたいなら、

npm install -g typedgate

とすると

typedgate --tsconfig ./tsconfig.json --src ./src/index.ts --json ./some-json-file.json

で実行できます。

Tips

巨大なプロジェクトで使う

--src には1つのファイルしか指定できませんが、このツールは内部的に TypeScript Compiler API を利用しているため、import export などで指定されたファイルもたどることができます。

そのため、例えば複数のファイルにインターフェイス定義がまたがっている場合、

export * from './user';
export * from './product';
export * from './login-history';
// other exports ...

などのように export をただ記載したファイルへのパスを指定して動作させることで、それぞれのファイルを辿ってバリデーションすることができます

上のファイルを index.ts とした場合、--src に指定するものは index.ts へのパスだけです。

複数の ControlComment 定義

例えば、以下のようなJSONがあった場合、

{
  "login":{
    "providers":{
      "twitter":{
        "url":"https://twitter.com/twogateinc",
        "follower":123
      },
      "facebook":{
        "url":"https://www.facebook.com/TwoGate-inc-646638978844172/",
        "follower":321
      },
      "instagram":{
        "url":"https://www.instagram.com/twogateinc/",
        "follower":987
      }
    }
  }
}

以下のような複数の ControlComment を利用して、interface 定義を複数箇所でバリデーションすることができます。

// @TG:path .login.providers.twitter
// @TG:path .login.providers.facebook
// @TG:path .login.providers.instagram
export interface ProviderItem {
  url: string;
  follower: number;
}

※本来であれば以下のように定義できれば良いのですが、Mapped型に未対応のため、現在はできません。

export type ProviderName = 'twitter' | 'facebook' | 'instagram'

// @TG:path .login <<-- こう書きたいところですが、動きません
export class SocialMedium {
  providers: { [key in ProviderName]: ProviderItem };
}

export interface ProviderItem {
  url: string;
  follower: number;
}

配列

このようなJSONがあった場合、

[
  {
    "num": 123,
  },
  {
    "num": 321,
  },
  {
    "num": 987,
  },
  {
    "num": 456,
  }
]

以下のように [] を指定することで配列を示すことができます

// @TG:path .[]
export interface ArrayInterface {
  num: number;
}

また、応用として例えば、

{
  "array":
    [
      {
        "num": 123,
      },
      {
        "num": 321,
      }
    ]
}

というようなJSONがあった場合、

// @TG:path .array[]
export interface ArrayInterface {
  num: number;
}

というように指定できます。この記法は、jq を参考にしています。

API

現在、本プログラムはCLIツールだけで、Validの場合は exit code = 0、 Not Valid の場合は exit code = 1 で終了するようになっていて、CIなどで利用できるようにPOSIXの流儀に則った挙動をするようになっています
ただ、CLIツールだと他のプログラムから利用しづらいため、他のnodeプログラムから import して使えるような API を準備しています。

制限

TypeScript Compiler API を利用していますが、TypeScript Compiler API で利用できるのがあくまで TypeScript の抽象構文木にアクセスすることまでで、実際の型のチェックは TypeScript の言語処理系を通している訳ではありません

プリミティブな型 (string,number,boolean,null) とそれのArray型はもちろん、 Union型 ('female' | 'male') 、Enum型には対応していますが、 Intersection型、Mapped型など複雑な型には対応していません。

ControlComment を付与できるものは Interface と Class 定義です。