openAPIの$refを展開できるようにした

前提・経緯

  • 個人でwebサービスを作るときに共通の型をフロントエンドとバックエンドで使いたかったので"openapi-typescript": "6.7.2"を使用
  • フォルダで区切ってschemaを管理したかったので$refを使ってる
  • 色々作ってたら何かしらのスキーマを展開した上でプロパティを追加したくなった

事前に定義されているデータ

// user
{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" }
  }
}
// post
{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "title": { "type": "string" }
  }
}

今回定義したいschema

userの情報に追加でlatestPostというpost型のデータを返したい

// userInfoAPI
{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" },
    "latestPost": {
      "type": "object",
      "properties": {
        "id": { "type": "string" },
        "title": { "type": "string" }
      }
    }
  }
}

愚直に$refを使った場合

  • schema
{
  "type": "object",
  "properties": {
    "$ref": "../schemas/user/model.json",
    "latestPost": { "$ref": "../schemas/post/model.json" }
  }
}
  • output.ts
"schemas/user/userInfo.json": {
  $ref?: external["schemas/user/model.json"];
  latestPost: external["schemas/post/model.json"];
};

展開した方法

apidevtools/json-schema-ref-parserを用いて中間で$refを展開したschemaを生成するようにした

具体的には$refが見つかったら解決して特定のkeyのオブジェクトを上書きしている

const replaceRefs = async (schema: JSONSchema) => {
  for (const [key, value] of Object.entries(schema)) {
    try {
      const resolverRef = await $RefParser.dereference(value["$ref"], options);
      Object.assign(schema, {
        [key]: {
          type: "object",
          properties: { ...resolverRef },
          required: requiredDataStore,
        },
      });
      requiredDataStore = [];
    } catch (error) {
      throw Error(error);
    }
  }
};

optionには読み込む時の処理を入れることができ、arrayの中に$refが入った時の処理を追加している (resolveArrayItemRefはこちらが定義した独自の関数)

const options: ParserOptions = {
  resolve: {
    file: {
      order: 1,
      read: async (fileInfo: FileInfo) => {
        const readResult = await fs.readFile(fileInfo.url, "utf-8");
        const parsedResult = JSON.parse(readResult);
        requiredDataStore = Array.from(
          new Set([...requiredDataStore, ...parsedResult["required"]]),
        );
        // $RefParser.dereferenceはおそらく"$ref"の時は自動で再帰的に解決してくれるが{ type:array, items:$ref }の時は見てくれなさそうなのでこちら側で対応する必要がある
        // なのでここでitemsの中に$refがあったら再帰的に処理するためにresolveArrayItemRefを呼び出す。
        await resolveArrayItemRef(parsedResult["properties"], fileInfo.url);
        return parsedResult["properties"];
      },
    },
  },
};

なんか問題になりそうなところ

自分がopenAPIを今使ってる限りでは簡単なことを表現するためにしか使ってないので問題になっていないが、oneOf, anyOf, allOf, not等を使う時にはparseが失敗するとかはありそう

感想

自分がどうにかならないかなと思っていた問題を解決できたのは嬉しかったけど、そもそもこういうケースになるのが自分の設計部分の間違いとか、openAPIの仕組みでにもっと良い解決方法があるとかは全然ある気がずっとしている…

あと、実装したのと記事を書くのにしばらく時間が空いていたのでかなりうる覚えで書いているところがある

参考