LoginSignup
1
1

More than 3 years have passed since last update.

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

Posted at

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 になるのです。

image.png

また、エラーが発生したソースファイルの場所と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 定義です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1