mcpField
A
add-malli-schemas

add-malli-schemas

github.com/metabase開発49.9k2.0k 回インストール今日更新
リポジトリ

add-malli-schemas について

MetabaseのコードベースでAPIエンドポイントにMalliスキーマを追加するスキルです。パラメーターやレスポンスの検証、スキーマの再利用、実データに基づくテストを案内します。

主な機能

  1. 01ルート・クエリ・本文パラメーターのスキーマ
  2. 02インラインおよび再利用可能なレスポンススキーマ
  3. 03Malli組み込みスキーマ型の案内
  4. 04検証タイミングとシリアライズの説明
  5. 05有効・無効なリクエストのテスト

活用シーン

  1. 01エンドポイントにパラメーター検証を追加
  2. 02実装データからレスポンススキーマを定義
  3. 03nullable・任意・日時フィールドへの対応
  4. 04エンドポイントテストでスキーマ動作を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/metabase/metabase --skill add-malli-schemas

インストール後、Claude Code で/add-malli-schemasを入力して読み込まれたか確認します。

詳細

API エンドポイントに Malli スキーマを追加する

このスキルでは、Metabase のコードベースにある API エンドポイントへ、Malli スキーマを効率的かつ一貫して追加する方法を説明します。

参考ファイル(優れた例)

  • src/metabase/warehouses_rest/api.clj - 最も包括的なスキーマとカスタムエラーメッセージ
  • src/metabase/api_keys/api.clj - 優れたレスポンススキーマ
  • src/metabase/collections_rest/api.clj - 優れた名前付きスキーマのパターン
  • src/metabase/timeline/api/timeline.clj - すっきりとしたシンプルな例

クイックチェックリスト

エンドポイントに Malli スキーマを追加する際は、次の点を確認してください。

  • ルートパラメーターにスキーマがある
  • クエリパラメーターに適切な :optional true と :default を指定したスキーマがある
  • リクエストボディにスキーマがある(POST/PUT の場合)
  • レスポンススキーマが定義されている(ルート文字列の後に :- を使用)
  • 可能な場合は ms 名前空間の既存のスキーマ型を使用している
  • 再利用可能な型や複雑な型には、名前付きスキーマの作成を検討している
  • バリデーション失敗時に、状況に応じたエラーメッセージを追加している

基本構造

エンドポイントの完全な例
(mr/def ::Color [:enum "red" "blue" "green"])

(mr/def ::ResponseSchema
  [:map
   [:id pos-int?]
   [:name string?]
   [:color ::Color]
   [:created_at ms/TemporalString]])

(api.macros/defendpoint :post "/:name" :- ::ResponseSchema
  "Create a resource with a given name."
  [;; Route Params:
   {:keys [name]} :- [:map [:name ms/NonBlankString]]
   ;; Query Params:
   {:keys [include archived]} :- [:map
                                   [:include  {:optional true} [:maybe [:= "details"]]]
                                   [:archived {:default false} [:maybe ms/BooleanValue]]]
   ;; Body Params:
   {:keys [color]} :- [:map [:color ::Color]]
   ]
  ;; endpoint implementation, ex:
  {:id 99
   :name (str "mr or mrs " name)
   :color ({"red" "blue" "blue" "green" "green" "red"} color)
   :created_at (t/format (t/formatter "yyyy-MM-dd'T'HH:mm:ssXXX") (t/zoned-date-time))}
  )

よく使うスキーマパターン

  1. ルートパラメーター(api/user/id/5 の 5)
  2. クエリパラメーター(api/users?sort=asc の sort+asc の組み合わせ)
  3. ボディパラメーター(リクエストボディの内容。ほとんどの場合、json から edn にデコードされます)
  4. 未加工のリクエストマップ

4 つの引数のうち、必要な場合を除き、未加工のリクエストの使用は優先度を下げてください。

ルートパラメーター

常に必須です。通常は ID を含むマップを指定します。

[{:keys [id]} :- [:map [:id ms/PositiveInt]]]

複数のルートパラメーターがある場合:

[{:keys [id field-id]} :- [:map
                           [:id ms/PositiveInt]
                           [:field-id ms/PositiveInt]]]
クエリパラメーター

{:optional true ...} と :default 値のプロパティを追加します。

{:keys [archived include limit offset]} :- [:map
                                            [:archived {:default false} [:maybe ms/BooleanValue]]
                                            [:include  {:optional true}   [:maybe [:= "tables"]]]
                                            [:limit    {:optional true} [:maybe ms/PositiveInt]]
                                            [:offset   {:optional true} [:maybe ms/PositiveInt]]]
リクエストボディ(POST/PUT)
{:keys [name description parent_id]} :- [:map
                                         [:name        ms/NonBlankString]
                                         [:description {:optional true} [:maybe ms/NonBlankString]]
                                         [:parent_id   {:optional true} [:maybe ms/PositiveInt]]]
レスポンススキーマ
シンプルなインラインレスポンス:
(api.macros/defendpoint :get "/:id" :- [:map
                                        [:id pos-int?]
                                        [:name string?]]
  "Get a thing"
  ...)
再利用する名前付きスキーマ:
(mr/def ::Thing
  [:map
   [:id pos-int?]
   [:name string?]
   [:description [:maybe string?]]])

(api.macros/defendpoint :get "/:id" :- ::Thing
  "Get a thing"
  ...)

(api.macros/defendpoint :get "/" :- [:sequential ::Thing]
  "Get all things"
  ...)

よく使うスキーマ型

metabase.util.malli.schema の型(ms としてエイリアス)

ms/* 名前空間のスキーマは API インフラストラクチャとの相性が良いため、優先して使用してください。

たとえば、pos-int? の代わりに ms/PositiveInt を使用します。

ms/PositiveInt                  ;; Positive integer
ms/NonBlankString               ;; Non-empty string
ms/BooleanValue                 ;; String "true"/"false" or boolean
ms/MaybeBooleanValue            ;; BooleanValue or nil
ms/TemporalString               ;; ISO-8601 date/time string (for REQUEST params only!)
ms/OpaqueJSONObject             ;; JSON object whose keys are not ours to declare; string-keyed
ms/PositiveNum                  ;; Positive number
ms/IntGreaterThanOrEqualToZero  ;; 0 or positive

重要: レスポンススキーマでは、時間関連のフィールドに ms/TemporalString ではなく :any を使用してください! レスポンススキーマは JSON シリアライズの前にバリデーションを行うため、Java Time オブジェクトを受け取ります。

Malli の組み込み型
:string                     ;; Any string
:boolean                    ;; true/false
:int                        ;; Any integer
:keyword                    ;; Clojure keyword
pos-int?                    ;; Positive integer predicate
[:maybe X]                  ;; X or nil
[:enum "a" "b" "c"]         ;; One of these values
[:or X Y]                   ;; Schema that satisfies X or Y
[:and X Y]                  ;; Schema that satisfies X and Y
[:sequential X]             ;; Sequential of Xs
[:set X]                    ;; Set of Xs
[:map-of K V]               ;; Map with keys w/ schema K and values w/ schema V
[:tuple X Y Z]              ;; Fixed-length tuple of schemas X Y Z

完全に必要な場合を除き、シーケンススキーマの使用は避けてください。

手順:エンドポイントにスキーマを追加する

例:GET /api/field/:id/related に戻り値のスキーマを追加する

変更前:

(api.macros/defendpoint :get "/:id/related"
  "Return related entities."
  [{:keys [id]} :- [:map [:id ms/PositiveInt]]]
  (-> (t2/select-one :model/Field :id id) api/read-check xrays/related))

手順 1: 関数が返す値を確認します(xrays/related を参照してください)

手順 2: 戻り値の型に基づいてレスポンススキーマを定義します。

(mr/def ::RelatedEntity
  [:map
   [:tables [:sequential [:map [:id pos-int?] [:name string?]]]]
   [:fields [:sequential [:map [:id pos-int?] [:name string?]]]]])

手順 3: エンドポイントにレスポンススキーマを追加します。

(api.macros/defendpoint :get "/:id/related" :- ::RelatedEntity
  "Return related entities."
  [{:keys [id]} :- [:map [:id ms/PositiveInt]]]
  (-> (t2/select-one :model/Field :id id) api/read-check xrays/related))

高度なパターン

カスタムエラーメッセージ
(def DBEngineString
  "Schema for a valid database engine name."
  (mu/with-api-error-message
   [:and
    ms/NonBlankString
    [:fn
     {:error/message "Valid database engine"}
     #(u/ignore-exceptions (driver/the-driver %))]]
   (deferred-tru "value must be a valid database engine.")))
ドキュメント付きの列挙型
(def PinnedState
  (into [:enum {:error/message "pinned state must be 'all', 'is_pinned', or 'is_not_pinned'"}]
        #{"all" "is_pinned" "is_not_pinned"}))
複雑なネストされたレスポンス
(mr/def ::DashboardQuestionCandidate
  [:map
   [:id ms/PositiveInt]
   [:name ms/NonBlankString]
   [:description [:maybe string?]]
   [:sole_dashboard_info
    [:map
     [:id ms/PositiveInt]
     [:name ms/NonBlankString]
     [:description [:maybe string?]]]]])

(mr/def ::DashboardQuestionCandidatesResponse
  [:map
   [:data [:sequential ::DashboardQuestionCandidate]]
   [:total ms/PositiveInt]])
ページネーション付きレスポンスのパターン
(mr/def ::PaginatedResponse
  [:map
   [:data [:sequential ::Item]]
   [:total integer?]
   [:limit {:optional true} [:maybe integer?]]
   [:offset {:optional true} [:maybe integer?]]])

よくある落とし穴

避ける:null 許容フィールドに :maybe を指定し忘れる
[:description ms/NonBlankString]  ;; WRONG - fails if nil
[:description [:maybe ms/NonBlankString]]  ;; RIGHT - allows nil
避ける:任意のクエリパラメーターに :optional true を指定し忘れる
[:limit ms/PositiveInt]  ;; WRONG - required but shouldn't be
[:limit {:optional true} [:maybe ms/PositiveInt]]  ;; RIGHT
避ける:既知のパラメーターに :default 値を指定し忘れる
[:limit ms/PositiveInt]  ;; WRONG - required but shouldn't be
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]]  ;; RIGHT
避ける:ルートパラメーター、クエリパラメーター、ボディを混同する
;; WRONG - all in one map
[{:keys [id name archived]} :- [:map ...]]

;; RIGHT - separate destructuring
[{:keys [id]} :- [:map [:id ms/PositiveInt]]
 {:keys [archived]} :- [:map [:archived {:default false} ms/BooleanValue]]
 {:keys [name]} :- [:map [:name ms/NonBlankString]]]
避ける:レスポンススキーマ内の Java Time オブジェクトに ms/TemporalString を使用する
;; WRONG - Java Time objects aren't strings yet
[:date_joined ms/TemporalString]

;; RIGHT - schemas validate BEFORE JSON serialization
[:date_joined :any]  ;; Java Time object, serialized to string by middleware
[:last_login [:maybe :any]]  ;; Java Time object or nil

理由: レスポンススキーマは、Clojure の内部データ構造が JSON にシリアライズされる前にバリデーションを行います。OffsetDateTime などの Java Time オブジェクトは、JSON ミドルウェアによって ISO-8601 文字列に変換されるため、スキーマでは変換前の Java オブジェクトを受け入れる必要があります。

避ける:データが実際にはセットであるのに [:sequential X] を使用する
;; WRONG - group_ids is actually a set
[:group_ids {:optional true} [:sequential pos-int?]]

;; RIGHT - matches the actual data structure
[:group_ids {:optional true} [:maybe [:set pos-int?]]]

理由: Toucan のハイドレーションメソッドは、セットを返すことがよくあります。JSON ミドルウェアはセットを配列にシリアライズしますが、スキーマのバリデーションはシリアライズ前に行われます。

避ける:再利用する構造に無名スキーマを作成する

複数箇所で使用するスキーマには mr/def を使用します。

(mr/def ::User
  [:map
   [:id pos-int?]
   [:email string?]
   [:name string?]])

戻り値の型を確認する

  1. 呼び出されている関数を確認する
(api.macros/defendpoint :get "/:id"
  [{:keys [id]}]
  (t2/select-one :model/Field :id id))  ;; Returns a Field instance
  1. 構造を確認するためにToucanのモデルを調べる

src/metabase/*/models/*.clj でモデル定義を確認します。

  1. clojure-mcpまたはREPLを使って調べる
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
  1. テストを確認する

テストには、期待されるレスポンスの構造が示されていることがよくあります。

スキーマ検証のタイミングを理解する

重要な概念: スキーマは、リクエスト/レスポンスのライフサイクルの異なる段階で検証されます。

リクエストパラメーターのスキーマ(Query/Body/Route)
  • JSONの解析後に検証されます
  • データはすでにデシリアライズされています(文字列、数値、ブール値)
  • 日付/時刻の入力には ms/TemporalString を使用します
  • ブール値のクエリパラメーターには ms/BooleanValue を使用します
レスポンスのスキーマ
  • JSONのシリアライズ前に検証されます
  • データはまだClojure形式です(Java Timeオブジェクト、セット、キーワード)
  • Java Timeオブジェクトには :any を使用します
  • セットには [:set X] を使用します
  • キーワードの列挙型には [:enum :keyword] を使用します
シリアライズのフロー
Request:  JSON string → Parse → Coerce → Handler
Response: Handler → Schema Check → Encode → Serialize → JSON string

ワークフローの概要

  1. エンドポイントを読む - 何をするのか理解します
  2. パラメーターを特定する - route、query、bodyを確認します
  3. パラメータースキーマを追加する - ms の既存の型を使用します
  4. 戻り値の型を特定する - 実装を確認します
  5. レスポンススキーマを定義する - インライン、または mr/def を使った名前付きスキーマにします
  6. テストする - エンドポイントが動作し、正しく検証されることを確認します

スキーマをテストする

スキーマを追加したら、次の点を確認します。

  1. 有効なリクエストが動作する - 正しいデータでテストします
  2. 無効なリクエストが適切に失敗する - 型が間違ったデータでテストします
  3. オプションのパラメーターが動作する - オプションのパラメーターあり/なしでテストします
  4. エラーメッセージが明確である - 検証エラーのレスポンスを確認します

ヒント

  • シンプルに始める - 基本的な型から始め、後で調整します
  • スキーマを再利用する - 同じ構造が2回出てきたら、名前付きスキーマにします
  • 具体的にする - pos-int? の代わりに ms/PositiveInt を使用します
  • 意図を記録する - 名前付きスキーマにdocstringを追加します
  • 規約に従う - 同じ名前空間にある類似のエンドポイントを確認します
  • 実際のデータを確認する - シリアライズ前に、REPLを使って実際に返される内容を調べます

その他のリソース

  • Malliのドキュメント
  • Metabase Malliユーティリティ:src/metabase/util/malli/schema.clj
  • Metabaseスキーマレジストリ:src/metabase/util/malli/registry.cljc

コメント