jsonschemaやopenAPI docのファイルを分解する機能を作った

github.com

背景

jsonschemaやopenAPI documentを触っている時に、時折、例としてあげられるファイルが巨大でインデントなどの対応を把握するのが辛くなることがある。

自分で書く場合には複数のファイルに分解された状態で書くので問題ないのだけれど。

手動で分解するのも面倒だったので。分解する機能をjsonknifeに追加した。

機能

separateという名前にした。適切な名前な気がしないのでもう少し良い名前を付けたい。 separateは過去に作ったbundleの機能と対になっている。

以下の様な対応関係。

  • bundle -- 複数のファイルをまとめて1つのファイルにする
  • separate -- 1つのファイルを分解して複数のファイルにする

例えば以下のようなJSONSchemaの定義があるとする。"#/definitions/address" の部分だけでも別ファイルになれば見やすいのになーと思ったりしたとする。

schema.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "address": {
      "properties": {
        "city": {
          "$ref": "#/definitions/name"
        },
        "state": {
          "type": "string"
        },
        "street_address": {
          "type": "string"
        }
      },
      "required": [
        "street_address",
        "city",
        "state"
      ],
      "type": "object"
    },
    "name": {
      "type": "string"
    }
  },
  "properties": {
    "billing_address": {
      "$ref": "#/definitions/address"
    },
    "shipping_address": {
      "$ref": "#/definitions/address"
    }
  },
  "type": "object"
}

このときにseparateを使って1つのファイル(schema.json)を複数のファイルに分解する。

$ jsonknife separate --src schema.json --format json --dst main.json
INFO:jsonknife.separator:emit file definitions/address.json
INFO:jsonknife.separator:emit file definitions/name.json
INFO:jsonknife.separator:emit file main.json

3つのファイルに分解された。

.
├── main.json
├── definitions
│   ├── address.json
│   └── name.json
└── schema.json  # これは元のファイル

分解された個々のファイルは以下のような内容になっている。

main.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "address": {
      "$ref": "definitions/address.json#/definitions/address"
    }
  },
  "properties": {
    "billing_address": {
      "$ref": "#/definitions/address"
    },
    "shipping_address": {
      "$ref": "#/definitions/address"
    }
  },
  "type": "object"
}

main.jsonがentrypoint的なもの。参照先は別のファイルになっているので全体の構造が把握しやすい。

(トップレベルのdefinitionsを削除してしまっても良い気もしたが残している。)

次に参照先のaddressの定義。ファイルの位置関係は元のファイルの改装構造とディレクトリの位置を対応させている。

definitions/address.json

{
  "definitions": {
    "address": {
      "properties": {
        "city": {
          "$ref": "name.json#/definitions/name"
        },
        "state": {
          "type": "string"
        },
        "street_address": {
          "type": "string"
        }
      },
      "required": [
        "street_address",
        "city",
        "state"
      ],
      "type": "object"
    }
  }
}

addressからしかnameは参照されないのでmain.jsonのdefinitionsにはnameが現れていない。

そのnameは以下のようなもの。

definitions/name.json

{
  "definitions": {
    "name": {
      "type": "string"
    }
  }
}

再帰的定義

再規定機な定義にも対応している。

例えば以下のようなファイル。

schema2.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "person": {
      "properties": {
        "children": {
          "default": [],
          "items": {
            "$ref": "#/definitions/person"
          },
          "type": "array"
        },
        "name": {
          "type": "string"
        }
      },
      "type": "object"
    }
  },
  "properties": {
    "person": {
      "$ref": "#/definitions/person"
    }
  },
  "type": "object"
}

これは以下のように(defaultはyamlで出力)

.
├── main2.yaml
├── definitions
│   └── person.yaml
└── schema2.json  # 元のファイル

main2.yaml

$schema: 'http://json-schema.org/draft-07/schema#'
definitions:
  person:
    $ref: 'definitions/person.yaml#/definitions/person'
properties:
  person:
    $ref: '#/definitions/person'
type: object

definitions/person.yaml

definitions:
  person:
    properties:
      children:
        default: []
        items:
          $ref: '#/definitions/person'
        type: array
      name:
        type: string
    type: object

複雑なもの

複雑なファイルに実行するとけっこうすごい感じになる。 たとえば speccyが持っているopenAPI3.0のjsonschemaを渡して分解したときに作られるファイルは以下の様なもの。

すごくたくさんのファイルが作られる。

.
├── main.json
├── definitions
│   ├── APIKeySecurityScheme.json
│   ├── AuthorizationCodeOAuthFlow.json
│   ├── BearerHTTPSecurityScheme.json
│   ├── Callback.json
│   ├── ClientCredentialsFlow.json
│   ├── Components.json
│   ├── Contact.json
│   ├── Discriminator.json
│   ├── Encoding.json
│   ├── Example.json
│   ├── ExternalDocumentation.json
│   ├── HTTPSecurityScheme.json
│   ├── Header.json
│   ├── HeaderWithContent.json
│   ├── HeaderWithSchema.json
│   ├── HeaderWithSchemaWithExample.json
│   ├── HeaderWithSchemaWithExamples.json
│   ├── ImplicitOAuthFlow.json
│   ├── Info.json
│   ├── License.json
│   ├── Link.json
│   ├── LinkWithOperationId.json
│   ├── LinkWithOperationRef.json
│   ├── MediaType.json
│   ├── MediaTypeWithExample.json
│   ├── MediaTypeWithExamples.json
│   ├── NonBearerHTTPSecurityScheme.json
│   ├── OAuth2SecurityScheme.json
│   ├── OAuthFlows.json
│   ├── OpenIdConnectSecurityScheme.json
│   ├── Operation.json
│   ├── Parameter.json
│   ├── ParameterWithContent.json
│   ├── ParameterWithContentInPath.json
│   ├── ParameterWithContentNotInPath.json
│   ├── ParameterWithSchema.json
│   ├── ParameterWithSchemaWithExample.json
│   ├── ParameterWithSchemaWithExampleInCookie.json
│   ├── ParameterWithSchemaWithExampleInHeader.json
│   ├── ParameterWithSchemaWithExampleInPath.json
│   ├── ParameterWithSchemaWithExampleInQuery.json
│   ├── ParameterWithSchemaWithExamples.json
│   ├── ParameterWithSchemaWithExamplesInCookie.json
│   ├── ParameterWithSchemaWithExamplesInHeader.json
│   ├── ParameterWithSchemaWithExamplesInPath.json
│   ├── ParameterWithSchemaWithExamplesInQuery.json
│   ├── PasswordOAuthFlow.json
│   ├── PathItem.json
│   ├── Paths.json
│   ├── Reference.json
│   ├── RequestBody.json
│   ├── Response.json
│   ├── Responses.json
│   ├── Schema.json
│   ├── SecurityRequirement.json
│   ├── SecurityScheme.json
│   ├── Server.json
│   ├── ServerVariable.json
│   ├── Tag.json
│   └── XML.json
└── schema.json  # 元のファイル

元のファイル(schema.json)と変換後のファイル(main.json)行数の差異。

$ wc schema.json main.json
 2346  5165 80453 schema.json
   71   145  1761 main.json
 2417  5310 82214 total

71行くらいなら全体を把握できる。

main.json

{
  "type": "object",
  "required": [
    "openapi",
    "info",
    "paths"
  ],
  "properties": {
    "openapi": {
      "type": "string",
      "pattern": "^3\\.0\\.\\d(-.+)?$"
    },
    "info": {
      "$ref": "#/definitions/Info"
    },
    "externalDocs": {
      "$ref": "#/definitions/ExternalDocumentation"
    },
    "servers": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/Server"
      }
    },
    "security": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/SecurityRequirement"
      }
    },
    "tags": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/Tag"
      }
    },
    "paths": {
      "$ref": "#/definitions/Paths"
    },
    "components": {
      "$ref": "#/definitions/Components"
    }
  },
  "patternProperties": {
    "^x-": {}
  },
  "additionalProperties": false,
  "definitions": {
    "Info": {
      "$ref": "definitions/Info.json#/definitions/Info"
    },
    "Server": {
      "$ref": "definitions/Server.json#/definitions/Server"
    },
    "Components": {
      "$ref": "definitions/Components.json#/definitions/Components"
    },
    "Paths": {
      "$ref": "definitions/Paths.json#/definitions/Paths"
    },
    "SecurityRequirement": {
      "$ref": "definitions/SecurityRequirement.json#/definitions/SecurityRequirement"
    },
    "Tag": {
      "$ref": "definitions/Tag.json#/definitions/Tag"
    },
    "ExternalDocumentation": {
      "$ref": "definitions/ExternalDocumentation.json#/definitions/ExternalDocumentation"
    }
  },
  "description": "This is the root document object for the API specification. It combines what previously was the Resource Listing and API Declaration (version 1.2 and earlier) together into one document."
}

テストは?

テストは実際に実行した結果のdiffを見てる。簡易的な回帰テスト。 (CIでもexamplesを全部実行した後にgit diffでdiffがなければOKというもの)

一応、元のファイルとseparate後bundleしたファイルが同じ内容なことを確認している。

https://github.com/podhmo/dictknife/blob/master/examples/jsonknife/separate/Makefile