PAYATHON 2026

如何在 compojure.api.sweet 中实现自定义 coercion?

数据小许

可以使用自定义 Schema coercion matcher。将 Datetime 定义为目标类型 java.time.ZonedDateTime,并为它提供字符串解析函数。Schema 会按照 Shift 的结构处理字段,:breaks 中的日期也会被转换。Schema coercion 说明

先确认 compojure-api 的版本

原问题没有提供依赖版本。下面的主要示例适用于提供 compojure.api.coercion.schema/create-coercion 的 2.x Schema coercion 接口。1.x 的扩展方式见后文,请勿混用两套配置。compojure-api Coercion 文档

定义目标类型和解析规则

Datetime 应描述转换后的对象类型。如果将它定义为 String,转换后的 ZonedDateTime 就无法通过类型校验。Schema 支持直接使用 Java 类作为类型约束。Schema 类型定义说明

(require '[schema.core :as s])
(import '[java.time ZonedDateTime]
        '[java.time.format DateTimeParseException])

(def Datetime ZonedDateTime)

(s/defschema Shift
  {:start Datetime
   :end Datetime
   (s/optional-key :breaks)
   [{:start Datetime
     :end Datetime}]})

(defn parse-datetime [value]
  (if (string? value)
    (try
      (ZonedDateTime/parse value)
      (catch DateTimeParseException _
        value))
    value))

(defn datetime-matcher [schema]
  (when (= schema Datetime)
    parse-datetime))

这里的 matcher 接收一个 Schema,匹配时返回转换函数,否则返回 nil。解析失败时,函数保留原字符串,由后续的 ZonedDateTime 类型校验报告错误。非字符串值也会接受类型校验。

将规则接入 JSON 请求体

将自定义 matcher 放在默认 JSON matcher 之前,可以处理日期并保留其他类型的默认转换规则。下面只修改 application/json 请求体的配置,并将 coercion 限定在 /shifts context 中。自定义 Schema coercion 配置

(require '[compojure.api.sweet
           :refer [api defroutes context POST]])
(require '[compojure.api.coercion.schema :as schema-coercion])

(def json-matcher
  (some-fn datetime-matcher
           schema-coercion/json-coercion-matcher))

(def shift-coercion
  (schema-coercion/create-coercion
    (assoc-in schema-coercion/default-options
              [:body :formats "application/json"]
              json-matcher)))

;; create-shift 沿用项目中已有的业务函数。
(defroutes http-routes
  (context "/shifts" []
    :coercion shift-coercion
    (POST "/" []
      :body [doc Shift]
      (create-shift doc))))

(def app
  (api http-routes))

服务入口应使用配置了请求格式解析和 coercion 的 handler。如果现有应用已通过 api 包装路由,保留原入口,在对应 context 上增加配置即可。

例如,使用 Content-Type: application/json 发送以下请求体:

{
  "start": "2026-01-12T09:00:00+08:00[Asia/Shanghai]",
  "end": "2026-01-12T18:00:00+08:00[Asia/Shanghai]",
  "breaks": [
    {
      "start": "2026-01-12T12:00:00+08:00[Asia/Shanghai]",
      "end": "2026-01-12T13:00:00+08:00[Asia/Shanghai]"
    }
  ]
}

在上述配置下,校验成功后,create-shift 接收到的 doc 中的这些日期字段应为 ZonedDateTime 对象。可以在开发环境检查 (class (:start doc)) 和 (class (get-in doc [:breaks 0 :start])),确认转换是否成功。

如果项目使用 1.x

对于使用 Ring-Swagger 默认 coercion matcher 的配置,可以通过 custom-matcher 扩展点复用前面的 parse-datetime:

(require '[ring.swagger.coerce :as swagger-coerce])

(defmethod swagger-coerce/custom-matcher ZonedDateTime [_]
  parse-datetime)

这会注册一个全局 multimethod 方法。要使其生效,实际使用的 coercion matcher 必须调用该扩展点。如果项目替换过默认 matcher,需要检查现有配置是否仍保留它。Ring-Swagger 自定义 coercion 说明

日期格式和错误处理

ZonedDateTime/parse 默认按 ISO_ZONED_DATE_TIME 解析。原问题没有说明输入格式,上面的日期字符串仅作示例。如果输入只有 2026-01-12T09:00:00,还需要明确时区,不能直接按示例解析为 ZonedDateTime;格式不同时,需要指定合适的 DateTimeFormatter。Java ZonedDateTime 文档

启用默认异常处理中间件时,请求 coercion 校验失败通常返回 HTTP 400,自定义异常处理可能改变这一行为。请求转换不负责响应的 JSON 序列化。如果 create-shift 返回日期对象,还需要配置编码方式;启用 Swagger 时,也要确认这些类型有对应的文档映射。compojure-api coercion 行为、Ring-Swagger 类型转换说明

备注:内容仅供参考。