关于 add-malli-schemas
面向 Metabase 代码库的技能,用于统一为 API 端点添加 Malli schema。它指导开发者验证参数和响应、复用 schema,并依据实际数据结构编写测试。
主要功能
- 01为路由、查询和请求体参数定义 schema
- 02定义内联及可复用的响应 schema
- 03介绍 Malli 内置 schema 类型
- 04说明验证时机与序列化流程
- 05测试有效和无效请求
适用场景
- 01为端点添加参数验证
- 02依据实现数据定义响应 schema
- 03处理可空、可选或时间字段
- 04通过端点测试检查 schema 行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ metabase/ metabase --skill add-malli-schemas安装后在 Claude Code 中输入/add-malli-schemas确认已加载。
详细介绍
为 API 端点添加 Malli Schema
此技能可帮助你高效、统一地为 Metabase 代码库中的 API 端点添加 Malli Schema。
参考文件(最佳示例)
src/metabase/warehouses_rest/api.clj- 最全面的 Schema 和自定义错误消息src/metabase/api_keys/api.clj- 出色的响应 Schemasrc/metabase/collections_rest/api.clj- 优秀的命名 Schema 模式src/metabase/timeline/api/timeline.clj- 简洁明了的示例
快速检查清单
为端点添加 Malli Schema 时:
- 路由参数有 Schema
- 查询参数有 Schema,并在适当时设置
:optional true和:default - 请求体有 Schema(适用于 POST/PUT)
- 已定义响应 Schema(使用路由字符串后面的
:-) - 尽可能使用
ms命名空间中的现有 Schema 类型 - 考虑为可复用或复杂类型创建命名 Schema
- 为验证失败添加上下文相关的错误消息
基本结构
完整端点示例
(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))}
)
常见 Schema 模式
- 路由参数(
api/user/id/5中的5) - 查询参数(
api/users?sort=asc中的sort+asc) - 请求体参数(请求体中的内容,几乎总是从 JSON 解码为 EDN)
- 原始请求映射
对于这 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]]]
响应 Schema
简单的内联响应:
(api.macros/defendpoint :get "/:id" :- [:map
[:id pos-int?]
[:name string?]]
"Get a thing"
...)
可复用的命名 Schema:
(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"
...)
常见 Schema 类型
来自 metabase.util.malli.schema(别名为 ms)
优先使用 ms/* 命名空间中的 Schema,因为它们与我们的 API 基础设施配合得更好。
例如,使用 ms/PositiveInt,而不是 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/OpaqueJSONObject ;; JSON object whose keys are not ours to declare; string-keyed
ms/PositiveNum ;; Positive number
ms/IntGreaterThanOrEqualToZero ;; 0 or positive
重要: 对于响应 Schema,时间字段请使用 :any,不要使用 ms/TemporalString!
响应 Schema 在 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
除非绝对必要,否则避免使用序列 Schema。
分步指南:为端点添加 Schema
示例:为 GET /api/field/:id/related 添加返回 Schema
之前:
(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: 根据返回类型定义响应 Schema:
(mr/def ::RelatedEntity
[:map
[:tables [:sequential [:map [:id pos-int?] [:name string?]]]]
[:fields [:sequential [:map [:id pos-int?] [:name string?]]]]])
步骤 3: 将响应 Schema 添加到端点:
(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
[: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]]]
不要:在响应 Schema 中对 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
原因: 响应 Schema 会在内部 Clojure 数据结构序列化为 JSON 之前进行验证。OffsetDateTime 等 Java Time 对象会由 JSON 中间件转换为 ISO-8601 字符串,因此 Schema 需要接受原始 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 中间件会将集合序列化为数组,但 Schema 会在序列化之前进行验证。
不要:为重复使用的结构创建匿名 Schema
对在多个位置使用的 Schema,使用 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)) ;; Returns a Field instance
- 查看 Toucan 模型的结构
在 src/metabase/*/models/*.clj 中查找模型定义。
- 使用 clojure-mcp 或 REPL 进行检查
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
- 查看测试
测试通常会展示预期的响应结构。
了解架构验证时机
关键概念: 架构会在请求/响应生命周期的不同阶段进行验证:
请求参数架构(查询/正文/路由)
- 在 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
工作流总结
- 阅读端点 - 了解其功能
- 识别参数 - 路由、查询、正文
- 添加参数架构 - 使用
ms中的现有类型 - 确定返回类型 - 检查实现
- 定义响应架构 - 内联定义,或使用
mr/def命名 - 测试 - 确保端点正常工作且验证正确
测试架构
添加架构后,请验证:
- 有效请求正常工作 - 使用正确的数据进行测试
- 无效请求能妥善失败 - 使用错误类型进行测试
- 可选参数正常工作 - 测试提供和不提供可选参数的情况
- 错误消息清晰易懂 - 检查验证错误响应
提示
- 从简单开始 - 先使用基本类型,之后再细化
- 重用架构 - 如果同一结构出现两次,就将其定义为命名架构
- 明确指定类型 - 使用
ms/PositiveInt,而不是pos-int? - 记录设计意图 - 为命名架构添加文档字符串
- 遵循约定 - 查看同一命名空间中的类似端点
- 检查实际数据 - 在序列化前使用 REPL 检查实际返回的数据
更多资源
- Malli 文档
- Metabase Malli 工具:
src/metabase/util/malli/schema.clj - Metabase 架构注册表:
src/metabase/util/malli/registry.cljc