add-malli-schemas

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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

参考文件(最佳示例)

  • 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
  • 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
    :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
为端点添加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

常见模式示例

  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.
  1. 路由参数(如
    api/user/id/5
    中的5)
  2. 查询参数(如
    api/users?sort=asc
    中的sort+asc对)
  3. 请求体参数(请求体内容,通常从JSON解码为EDN格式)
  4. 原始请求映射
在这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
{:optional true ...}
and
:default
values:
clojure
{: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 ...}
:default
属性:
clojure
{: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.schema
(别名为
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?
.
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 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.
优先使用ms/*命名空间中的模式,因为它们更适配我们的API基础设施。
例如,使用
ms/PositiveInt
而非
pos-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或正整数
重要提示: 对于响应模式,时间字段使用
:any
而非
ms/TemporalString
! 响应模式在JSON序列化之前验证,因此会检测到Java Time对象。

Built-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 Z
Avoid 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
添加返回模式

Before:
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/related
)
Step 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

clojure
[:description ms/NonBlankString]  ;; WRONG - fails if nil
[:description [:maybe ms/NonBlankString]]  ;; RIGHT - allows nil
clojure
[:description ms/NonBlankString]  ;; 错误 - 为nil时会验证失败
[:description [:maybe ms/NonBlankString]]  ;; 正确 - 允许为nil

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

错误:可选查询参数忘记添加
:optional true

clojure
[:limit ms/PositiveInt]  ;; WRONG - required but shouldn't be
[:limit {:optional true} [:maybe ms/PositiveInt]]  ;; RIGHT
clojure
[:limit ms/PositiveInt]  ;; 错误 - 被标记为必填,但实际应为可选
[:limit {:optional true} [:maybe ms/PositiveInt]]  ;; 正确

Don't: Forget
:default
values for known params

错误:已知参数忘记添加
:default

clojure
[:limit ms/PositiveInt]  ;; WRONG - required but shouldn't be
[:limit {:optional true :default 0} [:maybe ms/PositiveInt]]  ;; RIGHT
clojure
[: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

错误:响应模式中为Java Time对象使用
ms/TemporalString

clojure
;; 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.
clojure
;; 错误 - Java Time对象还不是字符串
[:date_joined ms/TemporalString]

;; 正确 - 模式在JSON序列化之前验证
[:date_joined :any]  ;; Java Time对象,由中间件序列化为字符串
[:last_login [:maybe :any]]  ;; Java Time对象或nil
原因: 响应模式验证的是内部Clojure数据结构,在JSON序列化之前。Java Time对象如
OffsetDateTime
会被JSON中间件转换为ISO-8601字符串,因此模式需要接受原始Java对象。

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

错误:当数据实际为集合时使用
[: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
mr/def
for schemas used in multiple places:
clojure
(mr/def ::User
  [:map
   [:id pos-int?]
   [:email string?]
   [:name string?]])
对在多个地方使用的结构,使用
mr/def
定义命名模式:
clojure
(mr/def ::User
  [:map
   [:id pos-int?]
   [:email string?]
   [:name string?]])

Finding Return Types

查找返回类型

  1. 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
  1. Check Toucan models for structure
Look in
src/metabase/*/models/*.clj
for model definitions.
  1. Use clojure-mcp or REPL to inspect
bash
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
  1. Check tests
Tests often show the expected response structure.
  1. 查看被调用的函数
clojure
(api.macros/defendpoint :get "/:id"
  [{:keys [id]}]
  (t2/select-one :model/Field :id id))  ;; 返回Field实例
  1. 查看Toucan模型结构
src/metabase/*/models/*.clj
中查找模型定义。
  1. 使用clojure-mcp或REPL检查
bash
./bin/mage -repl '(require '\''metabase.xrays.core) (doc metabase.xrays.core/related)'
  1. 查看测试用例
测试用例通常会展示预期的响应结构。

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
  • 在JSON解析之后验证
  • 数据已完成反序列化(字符串、数字、布尔值)
  • 日期/时间输入使用
    ms/TemporalString
  • 布尔查询参数使用
    ms/BooleanValue

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
  • 在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

工作流程总结

  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
  1. 阅读端点 - 理解其功能
  2. 识别参数 - 路由、查询、请求体
  3. 添加参数模式 - 使用
    ms
    中的现有类型
  4. 确定返回类型 - 查看实现代码
  5. 定义响应模式 - 内联或使用
    mr/def
    定义命名模式
  6. 测试 - 确保端点正常工作且验证逻辑正确

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
添加模式后,验证以下内容:
  1. 有效请求可正常工作 - 使用正确数据测试
  2. 无效请求可优雅失败 - 使用错误类型数据测试
  3. 可选参数正常工作 - 测试包含/不包含可选参数的情况
  4. 错误消息清晰 - 检查验证错误响应

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
  • 从简单开始 - 先使用基础类型,后续再优化
  • 复用模式 - 如果同一结构出现两次,将其定义为命名模式
  • 尽可能具体 - 使用
    ms/PositiveInt
    而非
    pos-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