add-malli-schemas
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdd Malli Schemas to API Endpoints
为API端点添加Malli模式
This skill helps you efficiently and uniformly add Malli schemas to API endpoints in the Metabase codebase.
本技能可帮助你在Metabase代码库中高效、统一地为API端点添加Malli模式。
Reference Files (Best Examples)
参考文件(最佳示例)
- - Most comprehensive schemas, custom error messages
src/metabase/warehouses/api.clj - - Excellent response schemas
src/metabase/api_keys/api.clj - - Great named schema patterns
src/metabase/collections/api.clj - - Clean, simple examples
src/metabase/timeline/api/timeline.clj
- - 最全面的模式、自定义错误消息
src/metabase/warehouses/api.clj - - 优秀的响应模式
src/metabase/api_keys/api.clj - - 出色的命名模式实践
src/metabase/collections/api.clj - - 简洁、易懂的示例
src/metabase/timeline/api/timeline.clj
Quick Checklist
快速检查清单
When adding Malli schemas to an endpoint:
- Route params have schemas
- Query params have schemas with and
:optional truewhere appropriate:default - Request body has a schema (for POST/PUT)
- Response schema is defined (using after route string)
:- - Use existing schema types from namespace when possible
ms - Consider creating named schemas for reusable or complex types
- Add contextual error messages for validation failures
为端点添加Malli模式时:
- 路由参数已配置模式
- 查询参数已配置模式,必要时添加和
:optional true:default - 请求体已配置模式(针对POST/PUT请求)
- 已定义响应模式(在路由字符串后使用)
:- - 尽可能使用命名空间中的现有模式类型
ms - 考虑为可复用或复杂类型创建命名模式
- 为验证失败添加上下文错误消息
Basic Structure
基础结构
Complete Endpoint Example
完整端点示例
clojure
(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))}
)clojure
(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
常见模式示例
- Route Params (the 5 in )
api/user/id/5 - Query Params (the sort+asc pair in )
api/users?sort=asc - Body Params (the contents of a request body. Almost always decoded from json into edn)
- The Raw Request map
Of the 4 arguments, deprioritize usage of the raw request unless necessary.
- 路由参数(如中的5)
api/user/id/5 - 查询参数(如中的sort+asc对)
api/users?sort=asc - 请求体参数(请求体内容,通常从JSON解码为EDN格式)
- 原始请求映射
在这4种参数中,除非必要,否则优先避免使用原始请求映射。
Route Params
路由参数
Always required, typically just a map with an ID:
clojure
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]For multiple route params:
clojure
[{:keys [id field-id]} :- [:map
[:id ms/PositiveInt]
[:field-id ms/PositiveInt]]]路由参数始终为必填项,通常仅包含ID的映射:
clojure
[{:keys [id]} :- [:map [:id ms/PositiveInt]]]多路由参数示例:
clojure
[{:keys [id field-id]} :- [:map
[:id ms/PositiveInt]
[:field-id ms/PositiveInt]]]Query Params
查询参数
Add properties for and values:
{:optional true ...}:defaultclojure
{: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]]]为可选参数添加和属性:
{:optional true ...}:defaultclojure
{: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)
请求体(POST/PUT)
clojure
{:keys [name description parent_id]} :- [:map
[:name ms/NonBlankString]
[:description {:optional true} [:maybe ms/NonBlankString]]
[:parent_id {:optional true} [:maybe ms/PositiveInt]]]clojure
{: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:
简单内联响应:
clojure
(api.macros/defendpoint :get "/:id" :- [:map
[:id pos-int?]
[:name string?]]
"Get a thing"
...)clojure
(api.macros/defendpoint :get "/:id" :- [:map
[:id pos-int?]
[:name string?]]
"Get a thing"
...)Named schema for reuse:
可复用的命名模式:
clojure
(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"
...)clojure
(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
)
metabase.util.malli.schemams来自metabase.util.malli.schema
(别名为ms
)
metabase.util.malli.schemamsPrefer the schemas in the ms/* namespace, since they work better with our api infrastructure.
For example use instead of .
ms/PositiveIntpos-int?clojure
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 positiveIMPORTANT: For response schemas, use for temporal fields, not !
Response schemas validate BEFORE JSON serialization, so they see Java Time objects.
:anyms/TemporalString优先使用ms/*命名空间中的模式,因为它们更适配我们的API基础设施。
例如,使用而非。
ms/PositiveIntpos-int?clojure
ms/PositiveInt ;; 正整数
ms/NonBlankString ;; 非空字符串
ms/BooleanValue ;; 字符串"true"/"false"或布尔值
ms/MaybeBooleanValue ;; 布尔值或nil
ms/TemporalString ;; ISO-8601日期/时间字符串(仅用于请求参数!)
ms/Map ;; 任意映射
ms/JSONString ;; JSON编码字符串
ms/PositiveNum ;; 正数
ms/IntGreaterThanOrEqualToZero ;; 0或正整数重要提示: 对于响应模式,时间字段使用而非!
响应模式在JSON序列化之前验证,因此会检测到Java Time对象。
:anyms/TemporalStringBuilt-in Malli Types
Malli内置类型
clojure
: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 ZAvoid using sequence schemas unless completely necessary.
clojure
: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除非完全必要,否则避免使用序列模式。
Step-by-Step: Adding Schemas to an Endpoint
分步指南:为端点添加模式
Example: Adding return schema to GET /api/field/:id/related
GET /api/field/:id/related示例:为GET /api/field/:id/related
添加返回模式
GET /api/field/:id/relatedBefore:
clojure
(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/relatedStep 2: Define response schema based on return type:
clojure
(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:
clojure
(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))添加前:
clojure
(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: 根据返回类型定义响应模式:
clojure
(mr/def ::RelatedEntity
[:map
[:tables [:sequential [:map [:id pos-int?] [:name string?]]]]
[:fields [:sequential [:map [:id pos-int?] [:name string?]]]]])步骤3: 为端点添加响应模式:
clojure
(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
自定义错误消息
clojure
(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.")))clojure
(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
带文档的枚举
clojure
(def PinnedState
(into [:enum {:error/message "pinned state must be 'all', 'is_pinned', or 'is_not_pinned'"}]
#{"all" "is_pinned" "is_not_pinned"}))clojure
(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
复杂嵌套响应
clojure
(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]])clojure
(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
分页响应模式
clojure
(mr/def ::PaginatedResponse
[:map
[:data [:sequential ::Item]]
[:total integer?]
[:limit {:optional true} [:maybe integer?]]
[:offset {:optional true} [:maybe integer?]]])clojure
(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
:maybe错误:可为空字段忘记添加:maybe
:maybeclojure
[:description ms/NonBlankString] ;; WRONG - fails if nil
[:description [:maybe ms/NonBlankString]] ;; RIGHT - allows nilclojure
[:description ms/NonBlankString] ;; 错误 - 为nil时会验证失败
[:description [:maybe ms/NonBlankString]] ;; 正确 - 允许为nilDon't: Forget :optional true
for optional query params
:optional true错误:可选查询参数忘记添加:optional true
:optional trueclojure
[:limit ms/PositiveInt] ;; WRONG - required but shouldn't be
[:limit {:optional true} [:maybe ms/PositiveInt]] ;; RIGHTclojure
[:limit ms/PositiveInt] ;; 错误 - 被标记为必填,但实际应为可选
[:limit {:optional true} [:maybe ms/PositiveInt]] ;; 正确Don't: Forget :default
values for known params
:default错误:已知参数忘记添加:default
值
:defaultclojure
[:limit ms/PositiveInt] ;; WRONG - required but shouldn't be
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]] ;; RIGHTclojure
[:limit ms/PositiveInt] ;; 错误 - 被标记为必填,但实际应为可选
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]] ;; 正确Don't: Mix up route params, query params, and body
错误:混淆路由参数、查询参数和请求体
clojure
;; 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]]]clojure
;; 错误 - 所有参数放在同一个映射中
[{: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]]]Don't: Use ms/TemporalString
for Java Time objects in response schemas
ms/TemporalString错误:响应模式中为Java Time对象使用ms/TemporalString
ms/TemporalStringclojure
;; 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 nilWhy: Response schemas validate the internal Clojure data structures BEFORE they are serialized to JSON. Java Time objects like get converted to ISO-8601 strings by the JSON middleware, so the schema needs to accept the raw Java objects.
OffsetDateTimeclojure
;; 错误 - Java Time对象还不是字符串
[:date_joined ms/TemporalString]
;; 正确 - 模式在JSON序列化之前验证
[:date_joined :any] ;; Java Time对象,由中间件序列化为字符串
[:last_login [:maybe :any]] ;; Java Time对象或nil原因: 响应模式验证的是内部Clojure数据结构,在JSON序列化之前。Java Time对象如会被JSON中间件转换为ISO-8601字符串,因此模式需要接受原始Java对象。
OffsetDateTimeDon't: Use [:sequential X]
when the data is actually a set
[:sequential X]错误:当数据实际为集合时使用[:sequential X]
[:sequential X]clojure
;; 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.
clojure
;; 错误 - group_ids实际是集合
[:group_ids {:optional true} [:sequential pos-int?]]
;; 正确 - 匹配实际数据结构
[:group_ids {:optional true} [:maybe [:set pos-int?]]]原因: Toucan的hydration方法通常返回集合。JSON中间件会将集合序列化为数组,但模式在序列化之前验证。
Don't: Create anonymous schemas for reused structures
错误:为可复用结构创建匿名模式
Use for schemas used in multiple places:
mr/defclojure
(mr/def ::User
[:map
[:id pos-int?]
[:email string?]
[:name string?]])对在多个地方使用的结构,使用定义命名模式:
mr/defclojure
(mr/def ::User
[:map
[:id pos-int?]
[:email string?]
[:name string?]])Finding Return Types
查找返回类型
- Look at the function being called
clojure
(api.macros/defendpoint :get "/:id"
[{:keys [id]}]
(t2/select-one :model/Field :id id)) ;; Returns a Field instance- Check Toucan models for structure
Look in for model definitions.
src/metabase/*/models/*.clj- Use clojure-mcp or REPL to inspect
bash
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'- Check tests
Tests often show the expected response structure.
- 查看被调用的函数
clojure
(api.macros/defendpoint :get "/:id"
[{:keys [id]}]
(t2/select-one :model/Field :id id)) ;; 返回Field实例- 查看Toucan模型结构
在中查找模型定义。
src/metabase/*/models/*.clj- 使用clojure-mcp或REPL检查
bash
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'- 查看测试用例
测试用例通常会展示预期的响应结构。
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 for date/time inputs
ms/TemporalString - Use for boolean query params
ms/BooleanValue
- 在JSON解析之后验证
- 数据已完成反序列化(字符串、数字、布尔值)
- 日期/时间输入使用
ms/TemporalString - 布尔查询参数使用
ms/BooleanValue
Response Schemas
响应模式
- Validate BEFORE JSON serialization
- Data is still in Clojure format (Java Time objects, sets, keywords)
- Use for Java Time objects
:any - Use for sets
[:set X] - Use for keyword enums
[:enum :keyword]
- 在JSON序列化之前验证
- 数据仍为Clojure格式(Java Time对象、集合、关键字)
- Java Time对象使用
:any - 集合使用
[:set X] - 关键字枚举使用
[:enum :keyword]
Serialization Flow
序列化流程
Request: JSON string → Parse → Coerce → Handler
Response: Handler → Schema Check → Encode → Serialize → JSON string请求: JSON字符串 → 解析 → 转换 → 处理器
响应: 处理器 → 模式检查 → 编码 → 序列化 → JSON字符串Workflow Summary
工作流程总结
- Read the endpoint - understand what it does
- Identify params - route, query, body
- Add parameter schemas - use existing types from
ms - Determine return type - check the implementation
- Define response schema - inline or named with
mr/def - Test - ensure the endpoint works and validates correctly
- 阅读端点 - 理解其功能
- 识别参数 - 路由、查询、请求体
- 添加参数模式 - 使用中的现有类型
ms - 确定返回类型 - 查看实现代码
- 定义响应模式 - 内联或使用定义命名模式
mr/def - 测试 - 确保端点正常工作且验证逻辑正确
Testing Your Schemas
测试你的模式
After adding schemas, verify:
- Valid requests work - test with correct data
- Invalid requests fail gracefully - test with wrong types
- Optional params work - test with/without optional params
- 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 instead of
ms/PositiveIntpos-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
- 从简单开始 - 先使用基础类型,后续再优化
- 复用模式 - 如果同一结构出现两次,将其定义为命名模式
- 尽可能具体 - 使用而非
ms/PositiveIntpos-int? - 记录意图 - 为命名模式添加文档字符串
- 遵循约定 - 查看同一命名空间中的类似端点
- 检查实际数据 - 使用REPL查看序列化前的实际返回数据
Additional Resources
额外资源
- Malli Documentation
- Metabase Malli utilities:
src/metabase/util/malli/schema.clj - Metabase schema registry:
src/metabase/util/malli/registry.clj
- Malli文档
- Metabase Malli工具类:
src/metabase/util/malli/schema.clj - Metabase模式注册表:
src/metabase/util/malli/registry.clj