説明書全部読む - Swagger Specification

ふとしです。

数年前から毎日エアロバイクを数時間漕いています。その暇つぶしにゲームばかりしていたのですが、さすがに時間の無駄遣いが過ぎると感じはじめて、去年の頭ぐらいからテレビに PC を繋いで本などを読めるようにしました。

説明書を全部読む

読めるものならなんでもいいので、読んでいる本がないときは、新しくしったライブラリのチュートリアルやリファレンスを上から下まで読むということをはじめました。

これが思った以上に有用で、特にライブラリのような手頃なサイズだとその後の試用や作業効率が非常に上がりました。もちろんすべて覚えられているわけではないのですが、こういうことができたはずだ、できないはずだというのがある程度わかるのである種の勘が働くようになります。

最近 Swagger をいじくる機会があったので Swagger を読んだ

先週は Swagger の定義をもとにエンドポイントを叩きまくるということをしていました。そこで、ちゃんと読んだことがないことに思い当たりました。特にパラメーターの定義部分はさまざまな設定があり、その全容がわからずに場当たり的に調べて書いていました。

今回通読することによってようやく全てのつながりが把握できた感じがします。

TypeScript で型にしてみる

読むだけではもったいないなと思ったので、写経の一種として https://swagger.io/specification をもとにコメントアウトを残しつつ TypeScript の Type に書き下してみました。(下部に全部貼っています)

なぜ TypeScript かというと、フィールドの値側を型の一部と見なして、ポリモーフィック (?) 的な型定義ができるため、type 属性によって設定できるフィールドや型がかわる Swagger と相性が良さそうだったからです。

例えば以下のように切りかえられる

type SchemaObject = SchemaObjectBooleanType | SchemaObjectStringType;

type SchemaObjectBooleanType = {
  type: 'boolean';
  example?: boolean;
};

type SchemaObjectStringType = {
  type: 'string';
  example?: string;
};

// ok
const a: SchemaObject = { type: 'string', example: 'abc' }
const b: SchemaObject = { type: 'boolean', example: false }

// めちゃくちゃに怒られる
const c: SchemaObject = { type: 'string', example: true }
// Type '{ type: "string"; example: true; }' is not assignable to type 'SchemaObject'.
//   Type '{ type: "string"; example: true; }' is not assignable to type 'SchemaObjectStringType'.
//     Types of property 'example' are incompatible.
//       Type 'true' is not assignable to type 'string | undefined'.

この機能により、どれがどの場合に必要とされているかしっかり把握できてかなり良かったです。

とくに SchemaObject は定義と JSON Schema Validation 由来のフィールドがとても多く、一緒に混ぜ込んだ状態でどれがいつ必要かを把握するのはむずかしかったでしょう。

書いたもの

精査はしていません。

一日ぐらい潰れましたが、やはり SchemaObject の全容を把握できたのは良かったです。

type RefString = string; // $ref
type SchemaName = string;

// primitive はオプションとして format 属性を持つ場合がある。
// format はその primitive の中身を規定する。
//
// その他定義上でのみ MUST とされているが JSON 上では区別できないものもあるので、
// 記述で区別するためにエイリアスを定義する。

type NonNegativeInteger = number;
type CommonMark = string;
type URLString = string;
type EmailString = string;
type TagString = string;
type OperationIDString = string; // ケースセンシティブ
type ContentTypeString = string;
type HTTPStatusCode = string;
type OperationRefString = string; // 特定の operation に対する URI
type RegularExpression = string;

type IntegerFormat = 'int32' | 'int62';
type NumberFormat = 'float' | 'double';
type StringFormat =
  | 'byte'
  | 'binary'
  | 'date'
  | 'password'
  | 'date-time'
  | 'email'
  | 'hostname'
  | 'ipv4'
  | 'ipv6';

// URL だが {} で囲まれた値は任意の値で置き換えられる。
type URLTemplateString = string;

// Schema により内容は変化するので any
type Example = any;

// OpenAPI Specification のルート。
type OpenAPIObject = {
  openapi: OpenAPIVersion;
  info: InfoObject;
  servers?: ServerObject[];
  paths: PathObject;
  components?: ComponentsObject;
  security?: SecurityRequirementObject[];
  tags?: TagObject[];
  externalDocs?: ExternalDocumentationObject;
};

// semantice version number。
type OpenAPIVersion = string;

// API に関するメタ情報を扱う。
type InfoObject = {
  title: string;
  description?: CommonMark;
  termsOfService?: URLString;
  contact?: ContactObject;
  license?: LicenseObject;
  version: DocumentVarsion;
};

type DocumentVarsion = string;

type ContactObject = {
  name?: string;
  url?: URLString;
  email?: EmailString;
};

type LicenseObject = {
  name: string;
  url?: URLString;
};

// エンドポイントサーバーの情報。
// variables は URL テンプレート記法を使っているときのデフォルト値などを規定する。
type ServerObject = {
  url: URLString | URLTemplateString;
  description?: CommonMark;
  variables?: { [k: string]: ServerVariableObject };
};

type ServerVariableObject = {
  enum?: string[]; // 値の候補。空にしないほうがよい。
  default: string;
  description?: CommonMark;
};

// 特に再利用可能な定義を保持しておくための要素。
//
// 全てのマップのキーは
//
//     ^[a-zA-Z0-9\.\-_]+$
//
// を満たす必要がある。
type ComponentsObject = {
  schemas?: { [k: string]: SchemaObject | ReferenceObject };
  responses?: { [k: string]: ResponseObject | ReferenceObject };
  parameters?: { [k: string]: ParameterObject | ReferenceObject };
  examples?: { [k: string]: ExampleObject | ReferenceObject };
  requestBodies?: { [k: string]: RequestBodyObject | ReferenceObject };
  headers?: { [k: string]: HeaderObject | ReferenceObject };
  securitySchemes?: { [k: string]: SecuritySchemeObject | ReferenceObject };
  links?: { [k: string]: LinkObject | ReferenceObject };
  callbacks?: { [k: string]: CallbackObject | ReferenceObject };
};

// 各パスを設定にマップするための要素。
// キーは必ずスラッシュから始まる /{path} となる。
//
// URL テンプレートと静的 URL では静的 URL が優先的にマッチする。
// 判別不能なあいまいな複数のパスの取扱いは未定義で、ツールの処理に依存する。
type PathObject = { [k: string]: PathItemObject };

// そのパスに規定されたオペレーションなどを定義する。
// OpenAPI の主要部分となる。
type PathItemObject = {
  $ref?: RefString;
  summary?: string;
  description?: CommonMark;
  get?: OperationObject;
  put?: OperationObject;
  post?: OperationObject;
  delete?: OperationObject;
  options?: OperationObject;
  head?: OperationObject;
  patch?: OperationObject;
  trace?: OperationObject;

  // このパス以下ではサーバー設定がこれらで上書きされる
  servers?: ServerObject[];

  // このパス以下で使われるパラメーターだが、オペレーションレベルで上書きできる。
  // name 属性において一意である必要がある
  parameters?: (ParameterObject | ReferenceObject)[];
};

type OperationObject = {
  tags?: TagString[];
  summary?: string;
  description?: CommonMark;
  externalDocs?: ExternalDocumentationObject;
  operationId?: OperationIDString;

  // PathItemObject にすでに定義されている場合、こちらが優先される。
  // PathItemObject と同じく name 属性において一意である必要がある。
  parameters?: (ParameterObject | ReferenceObject)[];

  requestBody?: RequestBodyObject | ReferenceObject;
  responses: ResponsesObject;
  callbacks?: { [k: string]: CallbackObject | ReferenceObject };
  deprecated?: boolean; // デフォルトは false
  security?: SecurityRequirementObject[];

  // このオペレーションではサーバー設定がこれらで上書きされる
  servers?: ServerObject[];
};

type ExternalDocumentationObject = {
  description?: CommonMark;
  url: URLString;
};

// 一つのオペレーションで使われるパラメーターを定義する。
// 一意性は name と location の組みあわせで判断する。
type ParameterObject = ParameterObjectCommon &
  ParameterObjectPath &
  ParameterObjectExample &
  ParameterObjectSchema;

type ParameterObjectCommon = {
  // ケースセンシティブ。
  // 任意の Object の parameters に含まれた場合、その中では name 属性において一意になる必要がある。
  // 一方で OpenAPI の定義全体を通して見た場合に一意でなくても構わない。
  //
  // in が path である場合、パスに表れている必要がある。
  // その他の場合、in が指す場所で使われるパラメーターの名前に従う。
  name: string;

  description?: CommonMark;
  deprecated?: boolean;
  allowEmptyValue?: boolean; // 今後削除される可能性がある属性であり、設定自体が非推奨

  style?: SerializationStyle;
  explode?: boolean; // Array か Object の場合、分割されたパラメーターを作成するか否か (?)
  allowReserved?: boolean; // 値に :/?#[]@!$&'()*+,;= を許すか否か。query にのみ意味があり、デフォルトでは false
};

type ParameterObjectPath =
  | {
      in: 'path';
      required: true; // Path 中に現れるのでパラメーターは必ず必須となる
    }
  | {
      in: 'query' | 'header' | 'cookie';
      required?: boolean;
    };

// example と examples は排他である。
// examples が schema から提供されている場合、ParameterObject 側のもので上書きする。
type ParameterObjectExample =
  | { example?: Example }
  | { examples?: { [k: string]: ExampleObject | ReferenceObject } };

// schema か content のいずれかを含まなければならない。
// content の場合はメディアタイプに応じた Schema が設定されることになる。
type ParameterObjectSchema =
  | { schema: SchemaObject | ReferenceObject }
  | { content: { [k: string]: MediaTypeObject } };

type SerializationStyle =
  | 'matrix'
  | 'label'
  | 'form'
  | 'simple'
  | 'spaceDelimited'
  | 'pipeDelimited'
  | 'deepObject';

type RequestBodyObject = {
  description?: CommonMark;
  content: { [k: string]: MediaTypeObject };
  required?: boolean;
};

// MediaType をキーとしたマップの値側になる要素。
// MediaType には `text/plain; charset=utf-8` `application/json` などがある。
type MediaTypeObject = {
  schema?: SchemaObject | ReferenceObject;

  // schema に含まれるプロパティのエンコーディング方法。
  // したがってキーは schema に含まれている必要がある。
  //
  // multipart か application/x-www-form-urlencoded の値として用いられている場合のみ
  // エンコーディング方法は適用されるべきである。
  encoding?: { [k: string]: EncodingObject };
} & ParameterObjectExample;

// 得られる各値についてのエンコーディング方法を設定する。
// 特に multipart ではメタ情報と画像など異なる形式のデータが入り交じるため、それに対応する。
type EncodingObject = {
  contentType?: ContentTypeString; // デフォルトではプロパティの type と format に依存する

  // 特に Content-Type は MediaType が multipart 以外では無視される。
  headers?: { [k: string]: HeaderObject | ReferenceObject };

  // 以下は ParameterObject と同様だが MediaType によっては無視される

  // MediaType が application/x-www-form-urlencoded 以外では無視される。
  style?: SerializationStyle;

  // MediaType が application/x-www-form-urlencoded 以外では無視される。
  explode?: boolean;

  // MediaType が application/x-www-form-urlencoded 以外では無視される。
  allowReserved?: boolean;
};

// ステータスコードをキーとしてレスポンスの内容を定義する。
// 定義していないステータスコードのレスポンスとして default をキーとして使用できる。
type ResponsesObject = { [k: string]: ResponseObject | ReferenceObject };

type ResponseObject = {
  // 他と違って必須である。
  description: CommonMark;

  headers?: { [k: string]: HeaderObject | ReferenceObject };
  content?: { [k: string]: MediaTypeObject | ReferenceObject };
  links?: { [k: string]: LinkObject | ReferenceObject }; // キーは ^[a-zA-Z0-9\.\-_]+$.
};

// よくわからないのでスルー
type CallbackObject = {};

type ExampleObject = {
  summary?: string;
  description?: CommonMark;
} & ExampleValue;

type ExampleValue = { value: any } | { externalValue: URLString };

// よくわからないのでスルー
type LinkObject = {
  parameters?: { [k: string]: any };
  requestBody?: any;
  description?: CommonMark;
  server?: ServerObject;
} & LinkOperation;

type LinkOperation =
  | { operationRef?: OperationRefString }
  | { operationId?: OperationIDString };

type HeaderObject = Omit<ParameterObject, 'name' | 'in'>;

// タグを前もって定義しておくための要素。
// Operation に Tag を設定する場合でも、この要素での定義は必須ではない。
type TagObject = {
  name: TagString;
  description?: CommonMark;
  externalDocs?: ExternalDocumentationObject;
};

// 絶対パスや相対パスが使える。
type ReferenceObject = {
  $ref: RefString;
};

// データ構造を定義する。
// 特に言及がない部分に関しては JSON Schema に準拠している。
//
// 以下のフィールドは JSON Schema 由来であり、定義も JSON Schema と同様である。
//
// - title
// - multipleOf
// - maximum
// - exclusiveMaximum
// - minimum
// - exclusiveMinimum
// - maxLength
// - minLength
// - pattern (This string SHOULD be a valid regular expression, according to the Ecma-262 Edition 5.1 regular expression dialect)
// - maxItems
// - minItems
// - uniqueItems
// - maxProperties
// - minProperties
// - required
// - enum
//
// 以下のフィールドは JSON Schema 由来だが OpenAPI 用に調整されている。
//
// - type - 文字列のみ
// - allOf - SchemaObject のみ含められる
// - oneOf - SchemaObject のみ含められる
// - anyOf - SchemaObject のみ含められる
// - not - SchemaObject のみ含められる
// - items - SchemaObject のみ。type が array の場合は必須となる。
// - properties - SchemaObject のみ
// - additionalProperties - boolean か SchemaObject のみ。JSON Schema による定義もできるがサポートしていない (?)
// - description - CommonMark を想定した文字列
// - format - JSON Schema のものに加えて OpenAPI 独自のものもある
// - default - 値が与えられなかった場合のデフォルト値。type に従った値のみが許容される。
//
// 以下は OpenAPI 独自のフィールドである
//
// - nullable - Null を許容する
// - discriminator - ポリモーフィズムにおける区別用の値 (?)
// - readOnly - true にした場合、リクエストに含めるべきではない値を表す。required に登録された値はレスポンスのみに含まれるようになる。
// - writeOnly - true にした場合、レスポンスに含めるべきではない値を表す。required に登録された値はリクエストのみに含まれるようになる。
// - xml - properties スキーマにのみ使われる。
// - externalDocs
// - example - 当 Schema のサンプル値
// - deprecated
type SchemaObject = SchemaObjectCommon &
  (
    | SchemaObjectBooleanType
    | SchemaObjectObjectType
    | SchemaObjectArrayType
    | SchemaObjectNumberType
    | SchemaObjectStringType
  );

type SchemaObjectCommon = {
  title?: string;
  description?: CommonMark;

  nullable?: boolean; // Null を許容するか否か。OpenAPI では type を一つしか設定できないのでこれで定義する。
  readOnly?: boolean; // レスポンスのみに含めるかどうか。
  writeOnly?: boolean; // リクエストのみに含めるかどうか。
  xml?: XMLObject; // properties に含まれるスキーマのみに使われ、該当する値のメタ情報を記述した XML を指定する。
  externalDocs?: ExternalDocumentationObject;
  deprecated: boolean;

  allOf?: SchemaObject[]; // 含まれるスキーマの全てを満たさなくてはならない。
  oneOf?: SchemaObject[]; // 含まれるスキーマのひとつだけを満たさなくてはならない。複数を満たした場合は invalid となる。
  anyOf?: SchemaObject[]; // 含まれるスキーマのいずれかを満たさなくてはならない。複数を満たしていても valid となる。
  not?: SchemaObject; // このスキーマを満たした場合は invalid となる。

  // type: SchemaType; // JSON Schema では配列が認められているが OpenAPI では型を指定する文字列のみ。null は含まれない。代わりに nullable 属性で設定する。
  // default?: any;    // 値が与えられなかったときのデフォルト値。type に応じた値になる。
  // example?: any;    // 参考値
  // format?: string;  // type によって要求できるフォーマットが異なる。指定された場合このフォーマットを満たさなくてはならない
  // enum?: any[];     // type によって中の型は変わる、value の候補。これが定義された場合、値はこの内の一つでなければならない。
};

type SchemaObjectBooleanType = {
  type: 'boolean';
  enum?: boolean[];
  default?: boolean;
  example?: boolean;
};

type SchemaObjectObjectType = {
  type: 'object';
  discriminator?: DiscriminatorObject; // ポリモーフィズムのための属性と、その属性の値がマッピングするスキーマを定義する。
  properties?: { [k: string]: SchemaObject }; // type が object のフィールドを定義する。省略した場合はフィールドがない object 扱いになる。
  maxProperties?: NonNegativeInteger; // これ以下のフィールド数でなくてはならない。
  minProperties?: NonNegativeInteger; // これ以上のフィールド数でなくてはならない。0 の場合は無視される。
  required?: string[]; // 必須フィールドを指定する。指定フィールド名は一意である必要がある。ここで指定された要素は全て含めなくてはならない。
  enum?: any[];
  default?: any;
  example?: any;
};

type SchemaObjectArrayType = {
  type: 'array';
  items?: SchemaObject; // type が array の場合は必須になる。含まれる要素のスキーマを定義する。
  maxItems?: NonNegativeInteger; // これ以下の要素数でなくてはならない。
  minItems?: NonNegativeInteger; // これ以上の要素数でなくてはならない。0 の場合は無視される。
  uniqueItems?: boolean; // 各要素は一意でなくてはならない。
  enum?: any[][];
  default?: any[];
  example?: any[];
};

type SchemaObjectNumberType = {
  type: 'number';
  format?: IntegerFormat | NumberFormat; // type によって要求できるフォーマットが異なる。指定された場合このフォーマットを満たさなくてはならない
  multipleOf?: number; // これで割り切れなくてはならない。
  maximum?: number; // これ以下でなくてはならない。
  exclusiveMaximum?: number; // これ未満でなくてはならない。
  minimum?: number; // これより大きくなくてはならない (この値を含まない)。
  exclusiveMinimum?: number; // これ以上でなくてはならない。
  enum?: number[];
  default?: number;
  example?: number;
};

type SchemaObjectStringType = {
  type: 'string';
  format?: StringFormat; // type によって要求できるフォーマットが異なる。指定された場合このフォーマットを満たさなくてはならない
  maxLength?: NonNegativeInteger; // これ以下の長さでなくてはならない。
  minLength?: NonNegativeInteger; // これ以上の長さでなくてはならない。0 の場合は無視される。
  pattern?: RegularExpression; // ECMA 262 regular expression に従う正規表現
  enum?: string[];
  default?: string;
  example?: string;
};

type DiscriminatorObject = {
  propertyName: string; // ポリモーフィズムで判断に使うフィールド名を指定する
  mapping?: { [k: string]: SchemaName | RefString }; // どのスキーマとして扱うかをマッピングする
};

// そのスキーマが XML をあらわしている時により詳細な定義を行うために使う
type XMLObject = {
  name?: string; // タグ名をすげかえる。type が array かつ wrapped が false の場合は (タグがないので) 無視される。
  namespace?: URLString; // namespace の定義を指定する
  prefix?: string; // name につく prefix。<prefix:name />
  attribute?: boolean; // 親に内包されるタグとしてではなく親の属性として定義する <prefix:name attribute="" />
  wrapped?: boolean; // type が array の時に items を囲うタグがあるか否かを決定する <items><item /><item /></items> or <item /><item />
};

// API 全体や特定のオペレーションのための認証・承認手続きを定義する。
// type により必須になる項目が異なる。
type SecuritySchemeObject = SecuritySchemeObjectCommon &
  (SecuritySchemeObjectApiKey | SecuritySchemeObjectHTTP);

type SecuritySchemeObjectCommon = {
  description?: CommonMark;
};

type SecuritySchemeObjectApiKey = {
  type: 'apiKey';
  name: string; // API Key が保持される名前を定義する
  in: 'query' | 'header' | 'cookie'; // API Key がどこに保持されているかを定義する
};

type SecuritySchemeObjectHTTP = {
  type: 'http';
} & (
  | SecuritySchemeObjectHTTPNormal
  | SecuritySchemeObjectHTTPBearer
  | SecuritySchemeObjectOAuth2
  | SecuritySchemeObjectOpenIDConnect
);

type SecuritySchemeObjectHTTPNormal = {
  // 使用するスキームを定義する。https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
  scheme:
    | 'basic'
    | 'digest'
    | 'hoba'
    | 'mutual'
    | 'negotiate'
    | 'oauth'
    | 'scram-sha-1'
    | 'scram-sha-256'
    | 'vapid';
};

type SecuritySchemeObjectHTTPBearer = {
  scheme: 'bearer';
  bearerFormat?: string; // bearer の時に bearer token のフォーマットを定義する。JWT など。
};

type SecuritySchemeObjectOAuth2 = {
  type: 'oauth2';
  flows: OauthFlowsObject; // OAuth についてはより詳細な手順を別オブジェクトで定義できる。
};

type SecuritySchemeObjectOpenIDConnect = {
  type: 'openIdConnect';
  openIdConnectUrl: URLString; // OAuth2 に関するコンフィグ値 (?) を得られる URL
};

// 各フローについて定義する。
type OauthFlowsObject = {
  // OAuth Implicit flow (なお OAuth として完全に使用しないことが推奨されている)
  // https://oauth.net/2/grant-types/implicit/
  implicit?: OauthFlowObject & OauthFlowObjectAuthorizationURL;

  // OAuth Resource Owner Password flow
  password?: OauthFlowObject & OauthFlowObjectTokenURL;

  // OAuth Client Credentials flow. OpenAPI 2.0 では application と呼ばれていた。
  clientCredentials?: OauthFlowObject & OauthFlowObjectTokenURL;

  // OAuth Authorization Code flow. OpenAPI 2.0 では accessCode と呼ばれていた。
  authorizationCode?: OauthFlowObject &
    OauthFlowObjectAuthorizationURL &
    OauthFlowObjectTokenURL;
};

type OauthFlowObject = {
  refreshUrl?: URLString; // リフレッシュトークンを得るための URL
  scopes: { [k: string]: string }; // その OAuth が定義しているスコープを列挙する。スコープ名をキーとして、値にはその説明を書く。
};

type OauthFlowObjectAuthorizationURL = {
  authorizationUrl: URLString; // フローで用いる認証用 URL
};

type OauthFlowObjectTokenURL = {
  tokenUrl: URLString; // フローで用いるトークンを得るための URL
};

// オペレーションが要求するセキュリティスキームを定義する。
// ComponentsObject で定義した securitySchemes でキーとしたものをキーとして使う。
//
// 指定した SecuritySchemeObject が oauth2 か openIdConnect だった場合、
// 値には必要とするスコープを列挙する。その他では空の配列となる。
type SecurityRequirementObject = { [k: string]: string[] };

おわりに

いわゆる API Console (Try it out) 機能を持つ OSS が意外と少ないので、これを機に自分で書いてみてもおもしろいかもしれません。

いまは material-ui の説明書を読んでいます。これも便利そうですね。