jpskill.com
🛠️ 開発・MCP コミュニティ

add-malli-schemas

Efficiently add Malli schemas to API endpoints in the Metabase codebase with proper patterns, validation timing, and error handling

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o add-malli-schemas.zip https://jpskill.com/download/19644.zip && unzip -o add-malli-schemas.zip && rm add-malli-schemas.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/19644.zip -OutFile "$d\add-malli-schemas.zip"; Expand-Archive "$d\add-malli-schemas.zip" -DestinationPath $d -Force; ri "$d\add-malli-schemas.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して add-malli-schemas.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → add-malli-schemas フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

[スキル名] add-malli-schemas

APIエンドポイントへのMalliスキーマの追加

このスキルは、Metabaseのコードベース内のAPIエンドポイントにMalliスキーマを効率的かつ統一的に追加するのに役立ちます。

参考ファイル(最良の例)

  • src/metabase/warehouses/api.clj - 最も包括的なスキーマ、カスタムエラーメッセージ
  • src/metabase/api_keys/api.clj - 優れたレスポンススキーマ
  • src/metabase/collections/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                  ;; 正の整数
ms/NonBlankString               ;; 空でない文字列
ms/BooleanValue                 ;; 文字列 "true"/"false" またはブール値
ms/MaybeBooleanValue            ;; BooleanValue または nil
ms/TemporalString               ;; ISO-8601日付/時刻文字列(リクエストパラメータのみ!)
ms/Map                          ;; 任意のマップ
ms/JSONString                   ;; JSONエンコードされた文字列
ms/PositiveNum                  ;; 正の数値
ms/IntGreaterThanOrEqualToZero  ;; 0または正の整数

重要: レスポンススキーマの場合、時間フィールドにはms/TemporalStringではなく:anyを使用してください! レスポンススキーマはJSONシリアライズの前に検証されるため、Java Timeオブジェクトを認識します。

組み込みのMalliタイプ

:string                     ;; 任意の文字列
:boolean                    ;; true/false
:int                        ;; 任意の整数
:keyword                    ;; Clojureキーワード
pos-int?                    ;; 正の整数述語
[:maybe X]                  ;; X または nil
[:enum "a" "b" "c"]         ;; これらの値のいずれか
[:or X Y]                   ;; X または Y を満たすスキーマ
[:and X Y]                  ;; X と Y の両方を満たすスキーマ
[:sequential X]             ;; X のシーケンシャル
[:set X]                    ;; X のセット
[:map-of K V]               ;; キーがスキーマ K、値がスキーマ V のマップ
[:tuple X Y Z]              ;; スキーマ 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 :
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Add Malli Schemas to API Endpoints

This skill helps you efficiently and uniformly add Malli schemas to API endpoints in the Metabase codebase.

Reference Files (Best Examples)

  • src/metabase/warehouses/api.clj - Most comprehensive schemas, custom error messages
  • src/metabase/api_keys/api.clj - Excellent response schemas
  • src/metabase/collections/api.clj - Great named schema patterns
  • src/metabase/timeline/api/timeline.clj - Clean, simple examples

Quick Checklist

When adding Malli schemas to an endpoint:

  • [ ] Route params have schemas
  • [ ] Query params have schemas with :optional true and :default where appropriate
  • [ ] Request body has a schema (for POST/PUT)
  • [ ] Response schema is defined (using :- after route string)
  • [ ] Use existing schema types from ms namespace when possible
  • [ ] Consider creating named schemas for reusable or complex types
  • [ ] Add contextual error messages for validation failures

Basic Structure

Complete Endpoint Example

(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))}
  )

Common Schema Patterns

  1. Route Params (the 5 in api/user/id/5)
  2. Query Params (the sort+asc pair in api/users?sort=asc)
  3. Body Params (the contents of a request body. Almost always decoded from json into edn)
  4. The Raw Request map

Of the 4 arguments, deprioritize usage of the raw request unless necessary.

Route Params

Always required, typically just a map with an ID:

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

For multiple route params:

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

Query Params

Add properties for {:optional true ...} and :default values:

{: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]]]

Request Body (POST/PUT)

{:keys [name description parent_id]} :- [:map
                                         [:name        ms/NonBlankString]
                                         [:description {:optional true} [:maybe ms/NonBlankString]]
                                         [:parent_id   {:optional true} [:maybe ms/PositiveInt]]]

Response Schemas

Simple inline response:

(api.macros/defendpoint :get "/:id" :- [:map
                                        [:id pos-int?]
                                        [:name string?]]
  "Get a thing"
  ...)

Named schema for reuse:

(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"
  ...)

Common Schema Types

From metabase.util.malli.schema (aliased as ms)

Prefer the schemas in the ms/* namespace, since they work better with our api infrastructure.

For example use ms/PositiveInt instead of pos-int?.

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/Map                          ;; Any map
ms/JSONString                   ;; JSON-encoded string
ms/PositiveNum                  ;; Positive number
ms/IntGreaterThanOrEqualToZero  ;; 0 or positive

IMPORTANT: For response schemas, use :any for temporal fields, not ms/TemporalString! Response schemas validate BEFORE JSON serialization, so they see Java Time objects.

Built-in Malli Types

: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

Avoid using sequence schemas unless completely necessary.

Step-by-Step: Adding Schemas to an Endpoint

Example: Adding return schema to GET /api/field/:id/related

Before:

(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))

Step 1: Check what the function returns (look at xrays/related)

Step 2: Define response schema based on return type:

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

Step 3: Add response schema to endpoint:

(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))

Advanced Patterns

Custom Error Messages

(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.")))

Enum with Documentation

(def PinnedState
  (into [:enum {:error/message "pinned state must be 'all', 'is_pinned', or 'is_not_pinned'"}]
        #{"all" "is_pinned" "is_not_pinned"}))

Complex Nested Response

(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]])

Paginated Response Pattern

(mr/def ::PaginatedResponse
  [:map
   [:data [:sequential ::Item]]
   [:total integer?]
   [:limit {:optional true} [:maybe integer?]]
   [:offset {:optional true} [:maybe integer?]]])

Common Pitfalls

Don't: Forget :maybe for nullable fields

[:description ms/NonBlankString]  ;; WRONG - fails if nil
[:description [:maybe ms/NonBlankString]]  ;; RIGHT - allows nil

Don't: Forget :optional true for optional query params

[:limit ms/PositiveInt]  ;; WRONG - required but shouldn't be
[:limit {:optional true} [:maybe ms/PositiveInt]]  ;; RIGHT

Don't: Forget :default values for known params

[:limit ms/PositiveInt]  ;; WRONG - required but shouldn't be
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]]  ;; RIGHT

Don't: Mix up route params, query params, and body

;; 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]]]

Don't: Use ms/TemporalString for Java Time objects in response schemas

;; 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

Why: Response schemas validate the internal Clojure data structures BEFORE they are serialized to JSON. Java Time objects like OffsetDateTime get converted to ISO-8601 strings by the JSON middleware, so the schema needs to accept the raw Java objects.

Don't: Use [:sequential X] when the data is actually a set

;; 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?]]]

Why: Toucan hydration methods often return sets. The JSON middleware will serialize sets to arrays, but the schema validates before serialization.

Don't: Create anonymous schemas for reused structures

Use mr/def for schemas used in multiple places:

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

Finding Return Types

  1. Look at the function being called
(api.macros/defendpoint :get "/:id"
  [{:keys [id]}]
  (t2/select-one :model/Field :id id))  ;; Returns a Field instance
  1. Check Toucan models for structure

Look in src/metabase/*/models/*.clj for model definitions.

  1. Use clojure-mcp or REPL to inspect
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
  1. Check tests

Tests often show the expected response structure.

Understanding Schema Validation Timing

CRITICAL CONCEPT: Schemas validate at different points in the request/response lifecycle:

Request Parameter Schemas (Query/Body/Route)

  • Validate AFTER JSON parsing
  • Data is already deserialized (strings, numbers, booleans)
  • Use ms/TemporalString for date/time inputs
  • Use ms/BooleanValue for boolean query params

Response Schemas

  • Validate BEFORE JSON serialization
  • Data is still in Clojure format (Java Time objects, sets, keywords)
  • Use :any for Java Time objects
  • Use [:set X] for sets
  • Use [:enum :keyword] for keyword enums

Serialization Flow

Request:  JSON string → Parse → Coerce → Handler
Response: Handler → Schema Check → Encode → Serialize → JSON string

Workflow Summary

  1. Read the endpoint - understand what it does
  2. Identify params - route, query, body
  3. Add parameter schemas - use existing types from ms
  4. Determine return type - check the implementation
  5. Define response schema - inline or named with mr/def
  6. Test - ensure the endpoint works and validates correctly

Testing Your Schemas

After adding schemas, verify:

  1. Valid requests work - test with correct data
  2. Invalid requests fail gracefully - test with wrong types
  3. Optional params work - test with/without optional params
  4. Error messages are clear - check validation error responses

Tips

  • Start simple - begin with basic types, refine later
  • Reuse schemas - if you see the same structure twice, make it a named schema
  • Be specific - use ms/PositiveInt instead of pos-int?
  • Document intent - add docstrings to named schemas
  • Follow conventions - look at similar endpoints in the same namespace
  • Check the actual data - use REPL to inspect what's actually returned before serialization

Additional Resources

  • Malli Documentation
  • Metabase Malli utilities: src/metabase/util/malli/schema.clj
  • Metabase schema registry: src/metabase/util/malli/registry.clj