TypeSpec を書いてて、OpenAPIスキーマの出力内容が意図通りになっていなくてハマったのでメモ。

TypeSpecでこういうAPI定義した時に、

types.tsp

model Widget {
  id: string;
  weight: int32;
  color: "red" | "blue";
}

main.tsp

import "@typespec/http";
import "./types.tsp";

using Http;
@service(#{ title: "Widget Service" })
namespace DemoService;

@route("/widgets")
interface Widget {
  @get read(@path id: string): Widget;
}

tsp compile . で生成されるOpenAPIスキーマで GET /widgets/{id} のレスポンスが無になる。

openapi: 3.1.0
info:
  title: Widget Service
  version: 0.0.0
tags: []
paths:
  /widgets/{id}:
    get:
      operationId: Widget_read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema: {}      # <- 無
components: {}

原因は main.tsp で、以下の修正で直せる。

 @route("/widgets")
-interface Widget {
+interface Widgets {
   @get read(@path id: string): Widget;
 }

正しく生成されたOpenAPIスキーマ

openapi: 3.1.0
info:
  title: Widget Service
  version: 0.0.0
tags: []
paths:
  /widgets/{id}:
    get:
      operationId: Widgets_read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Widget'   # <- 無じゃない!
(以下略)

いや、気づけよ、という話ではあるんだけど。

model定義とinterface定義が同じファイルの場合、 tsp compile . がエラーになるので容易に検知できる。

こういうmain.tspだと、

import "@typespec/http";

using Http;

@service(#{ title: "Widget Service" })
namespace DemoService;

model Widget {
  id: string;
  weight: int32;
  color: "red" | "blue";
}

@route("/widgets")
interface Widget {
  @get read(@path id: string): Widget;
}

こういうふうにエラーが出る。

$ tsp compile .
TypeSpec compiler v1.13.0

× Compiling
Diagnostics were reported during compilation:

main.tsp:8:7 - error duplicate-symbol: Duplicate name: "Widget"
> 8 | model Widget {
    |       ^^^^^^
main.tsp:15:11 - error duplicate-symbol: Duplicate name: "Widget"
> 15 | interface Widget {
     |           ^^^^^^

Found 2 errors.

modelとinterfaceの名前空間は同じなので、ファイルを分けるとそのチェックが働かなくなるみたい。

これ起因で、interface定義によっては↓みたいな謎の警告が出たりもする。 (どういう時に出るかまだ踏み込めてない)

main.tsp:15:19 - warning @typespec/http/metadata-ignored: path property will be ignored as it is inside of a @body property. Use @bodyRoot instead if wanting to mix.
> 15 |   @get read(@path id: string): Widget | Error;
     |                   ^^

これを避けるために、interface定義とmodel定義を同じファイルにしよう、というのは無理筋というか、ファイル構成に対する重い制約になってしまう。namespace分けるという手もあるが、API定義に反映されてしまうしな...

というわけで、一旦はinterface名は複数形にしておく、というルールにしておこうかな...
もう一度ハマったら、linter 作るかも。