RESTfulAPIで受け取るJSONの型定義を考えてみる

本稿はJamstackの話ではなくTypeScriptの話になるのですが、PowerCMS XのRESTfulAPIで受け取ったJSONの型定義について考えてみました。Fetch APIでリソースを取得した後response.json()を実行すると、そのままではunknown型になってしまいます。そこで、記事「Type-safe Data Fetching with unknown in TypeScript | Building SPAs」を参考にtype predicateを書くと、自身で定義した型エイリアス(もしくはnull)で受け取ることができ、コードのエラーを発見できたりエディタの補完が効いたりするなどの恩恵が得られます。

さて、型定義について考えていきます。検討用に以下のようなカラムを持つモデルを作成しました。

  • タイトル(テキスト255)
  • 本文(テキスト)
  • ファイル(バイナリ)
  • 単一ファイルリレーション(数値・アセットモデルへのリレーション)
  • 複数ファイルリレーション(リレーション・アセットモデルへのリレーション)

このモデルのオブジェクト1つをRESTful APIで取得すると、長くなるのですが以下のようなJSONが得られます。

{
    "id": 1,
    "title": "\u8a2d\u8a08\u7528\u30aa\u30d6\u30b8\u30a7\u30af\u30c8",
    "text": "\u003Cp\u003E\u672c\u6587\u304c\u5165\u308a\u307e\u3059\u3002\u003C\/p\u003E",
    "file": {
        "Url": "https:\/\/example.com\/assets\/design_type_definition\/design_type_definition.jpg",
        "Label": null,
        "Metadata": {
            "file_size": 733457,
            "image_width": 2048,
            "image_height": 1365,
            "class": "image",
            "extension": "jpg",
            "uploaded": "2021-10-19 17:50:18",
            "user_id": 1,
            "mime_type": "image\/jpeg"
        }
    },
    "single_relation_file": {
        "id": 7,
        "label": "\u7a7a\u306e\u99c5\u30aa\u30fc\u30c1\u30e3\u30fc\u30c9",
        "description": "",
        "file": {
            "Url": "https:\/\/example.com\/sites1\/assets\/images\/pic_orchard.jpg",
            "Label": null,
            "Metadata": {
                "file_size": 8249438,
                "image_width": 2571,
                "image_height": 2563,
                "class": "image",
                "extension": "jpg",
                "mime_type": "image\/jpeg",
                "uploaded": "2021-09-17 11:00:29",
                "user_id": 1
            }
        },
        "extra_path": "assets\/images\/",
        "file_name": "pic_orchard.jpg",
        "file_ext": "jpg",
        "mime_type": "image\/jpeg",
        "tags": [
            "@diary"
        ],
        "size": 8249438,
        "image_width": 2571,
        "image_height": 2563,
        "class": "image",
        "status": 4,
        "created_by": 1,
        "rev_note": "",
        "modified_by": 1,
        "rev_diff": "",
        "created_on": "2021-09-17 11:03:03",
        "modified_on": "2021-09-17 11:03:03",
        "workspace_id": 1,
        "has_deadline": 0,
        "published_on": "2021-09-17 11:00:28",
        "unpublished_on": null,
        "rev_type": 0,
        "rev_object_id": 0,
        "rev_changed": "",
        "user_id": 1,
        "previous_owner": 0,
        "uuid": "8ae4aebf-4596-4583-bc54-0692626848b7",
        "Permalink": "https:\/\/example.com\/sites1\/assets\/images\/pic_orchard.jpg"
    },
    "multiple_relation_file": [
        {
            "id": 6,
            "label": "\u4f38\u3073\u3092\u3059\u308b\u91ce\u826f\u732b",
            "description": "",
            "file": {
                "Url": "https:\/\/example.com\/sites1\/assets\/images\/pic_cat.jpg",
                "Label": null,
                "Metadata": {
                    "file_size": 7070101,
                    "image_width": 2352,
                    "image_height": 2352,
                    "class": "image",
                    "extension": "jpg",
                    "mime_type": "image\/jpeg",
                    "uploaded": "2021-09-17 11:00:28",
                    "user_id": 1
                }
            },
            "extra_path": "assets\/images\/",
            "file_name": "pic_cat.jpg",
            "file_ext": "jpg",
            "mime_type": "image\/jpeg",
            "tags": [
                "@diary"
            ],
            "size": 7070101,
            "image_width": 2352,
            "image_height": 2352,
            "class": "image",
            "status": 4,
            "created_by": 1,
            "rev_note": "",
            "modified_by": 1,
            "rev_diff": "",
            "created_on": "2021-09-17 11:02:29",
            "modified_on": "2021-09-17 11:02:29",
            "workspace_id": 1,
            "has_deadline": 0,
            "published_on": "2021-09-17 11:00:28",
            "unpublished_on": null,
            "rev_type": 0,
            "rev_object_id": 0,
            "rev_changed": "",
            "user_id": 1,
            "previous_owner": 0,
            "uuid": "f152c7b3-04fc-4481-8a31-0ec5ad070c61",
            "Permalink": "https:\/\/example.com\/sites1\/assets\/images\/pic_cat.jpg"
        },
        {
            "id": 5,
            "label": "\u30b0\u30ea\u30fc\u30f3\u30e9\u30a4\u30f3\u304b\u3089\u5185\u6d77\u5927\u6a4b\u304c\u898b\u3048\u305f",
            "description": "",
            "file": {
                "Url": "https:\/\/example.com\/sites1\/assets\/images\/pic_utsumi.jpg",
                "Label": null,
                "Metadata": {
                    "file_size": 4091734,
                    "image_width": 2675,
                    "image_height": 2675,
                    "class": "image",
                    "extension": "jpg",
                    "mime_type": "image\/jpeg",
                    "uploaded": "2021-09-17 11:00:28",
                    "user_id": 1
                }
            },
            "extra_path": "assets\/images\/",
            "file_name": "pic_utsumi.jpg",
            "file_ext": "jpg",
            "mime_type": "image\/jpeg",
            "tags": [
                "@diary"
            ],
            "size": 4091734,
            "image_width": 2675,
            "image_height": 2675,
            "class": "image",
            "status": 4,
            "created_by": 1,
            "rev_note": "",
            "modified_by": 1,
            "rev_diff": "",
            "created_on": "2021-09-17 11:02:04",
            "modified_on": "2021-09-17 11:02:04",
            "workspace_id": 1,
            "has_deadline": 0,
            "published_on": "2021-09-17 11:00:28",
            "unpublished_on": null,
            "rev_type": 0,
            "rev_object_id": 0,
            "rev_changed": "",
            "user_id": 1,
            "previous_owner": 0,
            "uuid": "74889c2f-d66b-4072-b186-7219dde98119",
            "Permalink": "https:\/\/example.com\/sites1\/assets\/images\/pic_utsumi.jpg"
        }
    ]
}

型定義を書いてみる

Sample型エイリアスとして書き始めます。typeなのかinterfaceなのかはまだ研究中ですが、ひとまずtypeにしておきます。IDはnumber、タイトル・本文はstringと特に迷うことはなさそうです。

type Sample = {
    id: number,
    title: string,
    text: string
};

バイナリタイプのカラムは項目が多いですが地道に型を当てはめるだけかと思います。Binaryに書き出してみます。UrlPermalinkがない場合があるので注意が必要です。

type Binary = {
    Url?: string,
    Label: string | null,
    Metadata: {
        file_size: number,
        image_width?: number,
        image_height?: number,
        class: string,
        extension: string,
        uploaded: string,
        user_id: number,
        mime_type: string
    }
};

数値タイプで単一ファイルのリレーション(アセットモデルへのリレーション)の場合も基本的には地道に型を当てはめていくのですが、アセットモデルのファイルカラムはバイナリタイプなので先程のBinaryが使い回せました。

type RelationAsset = {
    id: number,
    label: string,
    description: string,
    file: Binary,
    extra_path: string,
    file_name: string,
    file_ext: string,
    mime_type: string,
    tags: string[],
    size: number
    image_width: number
    image_height: number
    class: string,
    status: number
    created_by: number
    rev_note: string,
    modified_by: number
    rev_diff: string,
    created_on: string,
    modified_on: string,
    workspace_id: number
    has_deadline: number
    published_on: string,
    unpublished_on: string | null,
    rev_type: number
    rev_object_id: number
    rev_changed: string,
    user_id: number
    previous_owner: number
    uuid: string,
    Permalink?: string
};

リレーションタイプで複数ファイルのリレーション(アセットモデルへのリレーション)の場合は、単一ファイルのリレーションの内容が配列になっていましたので、RelationAsset[]です。

よって、Sample型エイリアスは以下のようになりました。

type Sample = {
    id: number,
    title: string,
    text: string,
    file: Binary,
    single_relation_file: RelationAsset,
    multiple_relation_file: RelationAsset[]
};

今後の課題

PowerCMS Xの特徴として自由にモデル・カラムが定義できるので、この型定義を使えば大丈夫というファイルをお渡しすることができません。リソースを取得する時のパラメータでカラムを限定するケースもあります。ただ、カラムタイプは限られていますし、先程紹介したバイナリタイプのカラムのように規則性もありそうです。地道に調査した後に自動生成を検討しAPIのエンドポイントを介して型定義を取得できないだろうか?(もしくはモデルの編集画面からエクスポートする)と考えています。json-schema-to-typescript等のツールについても調べてみようと思います。