add-malli-schemas by metabase/metabase
npx skills add https://github.com/metabase/metabase --skill add-malli-schemas此技能可帮助您在 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:-)广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
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
"创建具有给定名称的资源。"
[;; 路由参数:
{:keys [name]} :- [:map [:name ms/NonBlankString]]
;; 查询参数:
{:keys [include archived]} :- [:map
[:include {:optional true} [:maybe [:= "details"]]]
[:archived {:default false} [:maybe ms/BooleanValue]]]
;; 请求体参数:
{:keys [color]} :- [:map [:color ::Color]]
]
;; 端点实现,例如:
{: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))}
)
api/user/id/5 中的 5)api/users?sort=asc 中的 sort+asc 对)在这 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]]]
{: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?]]
"获取一个东西"
...)
(mr/def ::Thing
[:map
[:id pos-int?]
[:name string?]
[:description [:maybe string?]]])
(api.macros/defendpoint :get "/:id" :- ::Thing
"获取一个东西"
...)
(api.macros/defendpoint :get "/" :- [:sequential ::Thing]
"获取所有东西"
...)
metabase.util.malli.schema(别名为 ms)优先使用 ms/* 命名空间中的模式,因为它们与我们的 API 基础设施配合得更好。
例如,使用 ms/PositiveInt 而不是 pos-int?。
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 或正数
重要: 对于响应模式,对时间字段使用 :any,而不是 ms/TemporalString!响应模式在 JSON 序列化之前进行验证,因此它们看到的是 Java Time 对象。
: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"
"返回相关实体。"
[{: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
"返回相关实体。"
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]
(-> (t2/select-one :model/Field :id id) api/read-check xrays/related))
(def DBEngineString
"有效数据库引擎名称的模式。"
(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?]]])
:maybe[:description ms/NonBlankString] ;; 错误 - 如果为 nil 则失败
[:description [:maybe ms/NonBlankString]] ;; 正确 - 允许 nil
:optional true[:limit ms/PositiveInt] ;; 错误 - 必需但不应该是
[:limit {:optional true} [:maybe ms/PositiveInt]] ;; 正确
:default 值[:limit ms/PositiveInt] ;; 错误 - 必需但不应该是
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]] ;; 正确
;; 错误 - 全部在一个映射中
[{:keys [id name archived]} :- [:map ...]]
;; 正确 - 单独的解构
[{:keys [id]} :- [:map [:id ms/PositiveInt]]
{:keys [archived]} :- [:map [:archived {:default false} ms/BooleanValue]]
{:keys [name]} :- [:map [:name ms/NonBlankString]]]
ms/TemporalString;; 错误 - Java Time 对象还不是字符串
[:date_joined ms/TemporalString]
;; 正确 - 模式在 JSON 序列化之前验证
[:date_joined :any] ;; Java Time 对象,由中间件序列化为字符串
[:last_login [:maybe :any]] ;; Java Time 对象或 nil
原因: 响应模式在内部 Clojure 数据结构被序列化为 JSON 之前对其进行验证。像 OffsetDateTime 这样的 Java Time 对象会被 JSON 中间件转换为 ISO-8601 字符串,因此模式需要接受原始的 Java 对象。
[:sequential X];; 错误 - group_ids 实际上是一个集合
[:group_ids {:optional true} [:sequential pos-int?]]
;; 正确 - 匹配实际的数据结构
[:group_ids {:optional true} [:maybe [:set pos-int?]]]
原因: Toucan 水合方法通常返回集合。JSON 中间件会将集合序列化为数组,但模式在序列化之前进行验证。
对在多个地方使用的模式使用 mr/def:
(mr/def ::User
[:map
[:id pos-int?]
[:email string?]
[:name string?]])
(api.macros/defendpoint :get "/:id"
[{:keys [id]}]
(t2/select-one :model/Field :id id)) ;; 返回一个 Field 实例
2. 检查 Toucan 模型的结构
查看 src/metabase/*/models/*.clj 中的模型定义。
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
测试通常显示预期的响应结构。
关键概念: 模式在请求/响应生命周期的不同点进行验证:
ms/TemporalStringms/BooleanValue:any[:set X][:enum :keyword]请求: JSON 字符串 → 解析 → 强制转换 → 处理器
响应: 处理器 → 模式检查 → 编码 → 序列化 → JSON 字符串
ms 中的现有类型mr/def 命名添加模式后,验证:
ms/PositiveInt 而不是 pos-int?src/metabase/util/malli/schema.cljsrc/metabase/util/malli/registry.clj每周安装次数
155
仓库
GitHub 星标数
46.5K
首次出现
2026年1月20日
安全审计
安装于
claude-code146
opencode145
gemini-cli139
codex136
cursor136
github-copilot131
This skill helps you efficiently and uniformly add Malli schemas to API endpoints in the Metabase codebase.
src/metabase/warehouses/api.clj - Most comprehensive schemas, custom error messagessrc/metabase/api_keys/api.clj - Excellent response schemassrc/metabase/collections/api.clj - Great named schema patternssrc/metabase/timeline/api/timeline.clj - Clean, simple examplesWhen adding Malli schemas to an endpoint:
:optional true and :default where appropriate:- after route string)ms namespace when possible(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))}
)
api/user/id/5)api/users?sort=asc)Of the 4 arguments, deprioritize usage of the raw request unless necessary.
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]]]
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]]]
{: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 (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.
: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.
GET /api/field/:id/relatedBefore:
(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))
(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?]]])
:maybe for nullable fields[:description ms/NonBlankString] ;; WRONG - fails if nil
[:description [:maybe ms/NonBlankString]] ;; RIGHT - allows nil
:optional true for optional query params[:limit ms/PositiveInt] ;; WRONG - required but shouldn't be
[:limit {:optional true} [:maybe ms/PositiveInt]] ;; RIGHT
:default values for known params[: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]]]
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.
[: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.
Use mr/def for schemas used in multiple places:
(mr/def ::User
[:map
[:id pos-int?]
[:email string?]
[:name string?]])
(api.macros/defendpoint :get "/:id"
[{:keys [id]}]
(t2/select-one :model/Field :id id)) ;; Returns a Field instance
2. Check Toucan models for structure
Look in src/metabase/*/models/*.clj for model definitions.
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
Tests often show the expected response structure.
CRITICAL CONCEPT: Schemas validate at different points in the request/response lifecycle:
ms/TemporalString for date/time inputsms/BooleanValue for boolean query params:any for Java Time objects[:set X] for sets[:enum :keyword] for keyword enumsRequest: JSON string → Parse → Coerce → Handler
Response: Handler → Schema Check → Encode → Serialize → JSON string
msmr/defAfter adding schemas, verify:
ms/PositiveInt instead of pos-int?src/metabase/util/malli/schema.cljsrc/metabase/util/malli/registry.cljWeekly Installs
155
Repository
GitHub Stars
46.5K
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code146
opencode145
gemini-cli139
codex136
cursor136
github-copilot131
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
152,900 周安装