TypeScript のような構文で OpenAPI のスキーマを定義する TypeSpec
TypeSepc は TypeScript にインスパイアされた言語で、開発者が親しみやすい構文で OpenAPI のスキーマを定義できます。モデルを使ってデータの構造を定義し、`@route` デコレーターを使って REST API のエンドポイントを定義します。
TypeSpec は TypeScript にインスパイアされた言語で、開発者が親しみやすい構文で OpenAPI v3.0 のスキーマを定義できます。
スキーマの定義は以下のようになります。
import "@typespec/http";
using TypeSpec.Http;
model User {
id: string;
name: string;
birthday?: utcDateTime;
address: Address;
}
model Address {
street: string;
city: string;
state: string;
zip: string;
}
@route("/users")
interface Users {
list(@query limit: int32, @query skip: int32): User[];
create(@body user: User): User;
get(@path id: string): User;
}
上記のコードをコンパイルすると、以下のような OpenAPI のスキーマが出力されます。
openapi: 3.0.0
info:
title: (title)
version: 0000-00-00
tags: []
paths:
/users:
get:
operationId: Users_list
parameters:
- name: limit
in: query
required: true
schema:
type: integer
format: int32
- name: skip
in: query
required: true
schema:
type: integer
format: int32
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
operationId: Users_create
parameters: []
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/users/{id}:
get:
operationId: Users_get
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
Address:
type: object
required:
- street
- city
- state
- zip
properties:
street:
type: string
city:
type: string
state:
type: string
zip:
type: string
User:
type: object
required:
- id
- name
- address
properties:
id:
type: string
name:
type: string
birthday:
type: string
format: date-time
address:
$ref: '#/components/schemas/Address'
また、TypeSpec は OpenAPI 形式だけでなく、JSON Schema や Protobuf、JSON RPC などの形式にも対応しています。
インストール
はじめに、TypeSpec コンパイラをインストールします。
npm install -g @typespec/compiler
以下のコマンドで tsp のプロジェクトを初期化します。
tsp init
テンプレートを選択するように聞かれるので、Generic Rest API
を選択します。
ライブラリはデフォルト選択のまま @typespec/http
、@typespec/rest
、@typespec/openapi3
を選択します。
? Update the libraries? ›
Instructions:
↑/↓: Highlight option
←/→/[space]: Toggle selection
a: Toggle all
enter/return: Complete answer
◉ @typespec/http
◉ @typespec/rest
◉ @typespec/openapi3
セットアップが完了すると、以下のファイルが生成されます。
.
├── main.tsp
├── package.json
└── tsconfig.yaml
.tsp
拡張子は TypeSpec のソースコードを表します。もし VSCode を使っている場合には、以下の拡張をインストールすることをおすすめします。
以下のコマンドでコンパイルします。
tsp compile .
デフォルトの状態では tsp-ouput/@typespec/openapi3
に OpenAPI のスキーマが出力されます。
openapi: 3.0.0
info:
title: (title)
version: 0000-00-00
tags: []
paths: {}
components: {}
REST API のスキーマを定義してみる
それでは TypeSpec で REST API のスキーマを定義していきましょう。tsp compile . --watch
コマンドを実行すると、ファイルの変更を検知して自動的にコンパイルしてくれます。
HTTP API とのバインディングには @typespec/http
ライブラリを使います。このライブラリには HTTP のルーティングを定義するための @route
デコレーターや、Body リクエストを表す body
モデルなどが含まれています。
ライブラリをインポートするには import
キーワードを使います。TypeScript の import
と同じく、ファイルパスが指定された場合には対応するファイルまたはディレクトリから探索されます。パッケージ名が指定された場合にはまず TypeSpec は package.json
を探索し、ライブラリのエントリーポイントを探します。
import "@typespec/http";
using
キーワードを使うことで namespace を現在のスコープに公開できます。これにより、@route
デコレーターなどパッケージで公開されているシンボルを参照できるようになります。
import "@typespec/http";
using TypeSpec.Http;
メタデータの定義
namespace に対して @typespec/http
パッケージが提供する @service
、@server
デコレーターを使うことで、OpenAPI のメタデータを定義できます。
import "@typespec/http";
using TypeSpec.Http;
@service({
title: "User API",
version: "1.0.0",
})
@server("https://example.com/api", "production")
namespace UserAPI;
コンパイルすると以下のように変換されます。
openapi: 3.0.0
info:
title: User API
version: 1.0.0
tags: []
paths: {}
components: {}
servers:
- url: https://example.com/api
description: production
variables: {}
モデルの定義
TypeSpec では モデル を使ってデータの構造を定義します。モデルは TypeScript のインターフェースと似たようなレコードの形式または配列です。
model User {
id: string;
name: string;
}
モデルはコンパイルされると components/schemas
として OpenAPI のスキーマに変換されます。
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: string
name:
type: string
ビルトインで用意されているプリミティブな型は Built-in Types を参照してください。別のモデルをデータ型として使うこともできます。
model User {
id: string;
name: string;
address: Address;
}
model Address {
street: string;
city: string;
state: string;
zip: string;
}
オプショナルなプロパティを定義するには ?
を使います。
model User {
id: string;
name: string;
birthday?: utcDateTime;
address: Address;
}
オプショナルなプロパティにはデフォルト値を指定できます。
model User {
id: string;
name: string;
birthday?: utcDateTime = "2000-01-01T00:00:00Z";
address: Address;
}
ビルトインのデコレーターを使うことで、プロパティに対してバリデーションを定義できます。
model User {
id: string;
@minLength(1)
@maxLength(10)
name: string;
birthday?: utcDateTime;
address: Address;
}
@doc
デコレーターはモデルやプロパティに対してドキュメントを定義します。
@doc("ユーザー")
model User {
id: string;
@doc("ユーザー名")
@minLength(1)
@maxLength(10)
name: string;
birthday?: utcDateTime;
address: Address;
}
またはドキュメントコメント(/** ... */
)を使うこともできます。
/**
* ユーザー
*/
model User {
id: string;
/**
* ユーザー名
*/
@minLength(1)
@maxLength(10)
name: string;
birthday?: utcDateTime;
address: Address;
}
スプレッド演算子(...
)を使うことで、他のモデルのプロパティをコンポジションできます。
model User {
id: string;
name: string;
birthday?: utcDateTime = "2000-01-01T00:00:00Z";
address: Address;
}
enum Role {
read,
write,
}
model Admin {
...User;
role: Role;
}
extends
キーワードを使うことで、他のモデルを継承できます。これは明示的に関係を表したい時に使います。
model User {
id: string;
name: string;
birthday?: utcDateTime = "2000-01-01T00:00:00Z";
address: Address;
}
enum Role {
read,
write,
}
model Admin extends User {
role: Role;
}
Template を使うことで、特定のパターンのデータ型を簡単に定義できます。例えば、ページネーションのレスポンスを表す Pagination
モデルを定義してみましょう。
model Pagination<T> {
items: T[];
total: int32;
offset: int32;
limit: int32;
count: int32;
}
この Pagination モデルは、T
という型パラメーターを持っています。この型パラメーターは Pagination
モデルを使う時に実際の型に置き換えられます。
mode UserPagination extends Pagination<User> {}
// => {
// items: User[];
// total: int32;
// offset: int32;
// limit: int32;
// count: int32;
// }
リソースの定義
リソースは REST API のエンドポイントを表します。リソースは @route
デコレーターを使って定義します。@route
デコレーターはパス名を引数に取ります。
@route("/users")
interface Users {}
interface
のメソッドとしてエンドポイントに対する操作を定義します。リクエストパラメータをメソッドの引数に、レスポンスをメソッドの戻り値として定義します。
@error
model Error {
code: int32;
message: string;
}
@route("/users")
interface Users {
// @query デコレーターはクエリパラメータ
list(@query limit: int32, @query skip: int32): Pagination<User>;
// @body デコレーターはリクエストボディ
create(@body user: User): User | Error;
// @path デコレーターはパスパラメータ
get(@path id: string): User | Error;
}
このコードをコンパイルすると以下のように変換されます。
paths:
/users:
get:
operationId: Users_list
parameters:
- name: limit
in: query
required: true
schema:
type: integer
format: int32
- name: skip
in: query
required: true
schema:
type: integer
format: int32
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
type: object
required:
- items
- total
- offset
- limit
- count
properties:
items:
type: array
items:
$ref: '#/components/schemas/User'
total:
type: integer
format: int32
offset:
type: integer
format: int32
limit:
type: integer
format: int32
count:
type: integer
format: int32
post:
operationId: Users_create
parameters: []
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/User'
- $ref: '#/components/schemas/Error'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/users/{id}:
get:
operationId: Users_get
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/User'
- $ref: '#/components/schemas/Error'
デコレーターにより HTTP メソッドを指定しない場合にはデフォルトで GET
メソッドとなり、@body
デコレーターが指定されている場合に限り POST
メソッドとして扱われます。
それぞれの HTTP メソッドに対応するデコレーターを使うことで、明示的に HTTP メソッドを指定できます。
@route("/users")
interface Users {
@get
list(@query limit: int32, @query skip: int32): Pagination<User>;
@post
create(@body user: User): User | Error;
@get
get(@path id: string): User | Error;
@put
update(@path id: string, @body user: User): User | Error;
@delete
delete(@path id: string): User | Error;
}
@header
デコレーターを使うとヘッダーを、statusCodes
デコレーターを使うとステータスコードそれぞれ定義できます。
@route("/users")
interface Users {
@get
list(@query limit: int32, @query skip: int32, @header ifMatch?: string): {
@header ETag: string;
@body pagenationUser: Pagination<User>;
};
@post
create(@body user: User): {
@statusCode statusCode: 201 | 400;
@body User: User | Error;
};
}
まとめ
- TypeSpec は TypeScript にインスパイアされた言語で、開発者が親しみやすい構文で OpenAPI のスキーマを定義できる
- モデルを使ってデータの構造を定義する
@route
デコレーターを使って REST API のエンドポイントを定義する