在 Scalatra 里把返回值改成 JSON,看起来只需要加上 json4s-jackson、scalatra-json 和几行配置,但真正上手时,常见问题往往不是“怎么写”,而是“为什么写了却没有生效”。这篇文章按实际开发中最容易踩到的几个点重组示例:先把最基本的 JSON 响应跑通,再分别处理未生效、非 case class、动态字段和 JSONP 场景,最后补一条更适合项目落地的组织方式,便于你快速判断该用哪种返回策略。
先把最基础的 JSON 响应跑通
先看一个最简单的实体定义,假设有这样一个 Entity:
case class Entity(var value: String)
在 Servlet 的请求中直接返回一个实体列表:
get("/") {
List(Entity("foo"), Entity("bar"))
}
如果没有做 JSON 相关配置,访问后得到的通常是这样的默认输出:
List(Entity(foo), Entity(bar))
这种格式对前端并不友好。要让 Scalatra 直接返回 JSON,官方文档建议使用 Scalatra 2.7 JSON 指南 中提到的方案,引入 json4s,并使用 json4s-jackson、scalatra-json。
随后在 Servlet 中定义一个隐式延迟常量 jsonFormats:
protected implicit lazy val jsonFormats: Formats = DefaultFormats
再实现 ScalatraBase 下的 before() 方法,明确响应类型:
before() {
contentType = formats("json")
}
正常情况下,这时就可以得到预期的 JSON 结果:
[{"value":"foo"},{"value":"bar"}]
为什么配置了还是没生效
这个问题很常见,而且麻烦在于它往往不会报错,只是继续输出默认字符串形式,让人误以为是别的环节出了问题。

先检查 scalatra-json 是否真的引入
如果没有成功引入 scalatra-json,那么 jsonFormats 和 formats("json") 其实都不会被正确处理。结果就是:程序不一定报错,但返回值仍旧是默认输出,而不是 JSON。

再检查 Servlet 是否混入了 JSON 支持
另一种常见情况,是 Servlet 本身没有使用 with JacksonJsonSupport。这种情况下,即便你已经写了 jsonFormats 和 before(),序列化链路仍然是不完整的。
换句话说,如果你已经照着文档写了配置,但接口返回仍然像这样:
List(Entity(foo), Entity(bar))
优先排查的不是业务代码,而是这两项基础接入是否到位:依赖有没有引入、Servlet 有没有混入 JacksonJsonSupport。
不是 case class 时怎么正确序列化
Scalatra 配合 json4s 时,对 case class 的支持比较直接;但如果你的实体不是 case class,行为就不一样了。
例如下面这种普通类:
class Entity(var value: String)
get("/") {
new Entity("foo")
}
它的响应结果通常会变成类似 Entity@xxx 的形式,而不会自动解析成 JSON。原因很直接:这里返回的是一个普通对象实例,框架没有帮你完成预期中的自动序列化。
这时可以显式调用 org.json4s.Serialization.write():
get("/") {
Serialization.write(new Entity("foo"))
}
这样就能得到正确结果:
{"value":"foo"}
由于 Serialization 这组 API 比较简洁,实践里可以直接整体导入,写法会更顺手:
import org.json4s.jackson.Serialization._
get("/") {
write(new Entity("foo"))
}
如果你的项目里既有适合建模的固定结构,也有一部分历史类或第三方对象不方便改成 case class,那么最简单的判断标准就是:
case class:优先走框架默认 JSON 输出;- 普通类:显式使用
write()做序列化。
动态 JSON 和 JSONP 分别怎么处理
字段不固定时,直接返回 JObject
有些接口返回内容非常灵活,字段是动态的。这种场景下,如果专门为返回值再定义一个实体类,成本不一定划算。更简单的做法是直接返回 JObject。
scalatra-json 可以自动处理这类结果,因此按 json4s 的方式组装即可。常见写法是用 JsonDSL 的 ~ 来拼 JSON:
import org.json4s.JsonDSL._
get("/") {
("foo" -> "1") ~ ("bar" -> "2")
}
得到的结果是:
{"foo":"1","bar":"2"}
这类方式适合轻量接口、拼装型响应,或者字段是否出现取决于运行时条件的场景。
需要 JSONP 时,定义回调参数名
如果还要兼容 JSONP,可以在 Servlet 中定义:
override def jsonpCallbackParameterNames = Seq("jsonp")
然后在请求参数中加上 jsonp,并把它的值作为回调函数名。例如:
http:/localhost/?jsonp=jsonpCallback
这时就可以得到类似下面的响应:
/**/jsonpCallback({"foo":"1","bar":"2"});
也就是说,JSONP 支持的关键不在于重新改写返回结构,而在于告诉框架:哪个参数名用来承载回调函数名。
更适合项目落地的组织方式
如果把 Servlet 视为 MVC 模式中的 Controller,那么更稳妥的做法通常不是在每个 Controller 里重复写一遍 JSON 配置,而是抽一个统一的 ControllerBase。
可以把 jsonFormats 放进这个基类里,再让所有 Controller 继承它。这样做的好处是:
- 减少重复配置;
- 避免某个新建 Controller 漏掉 JSON 支持;
- 后续扩展统一格式时更容易维护。
这一组织方式可以参考项目 Uniliva/tcc-fatec-back-end-scala。
如果还需要对某些类型做特殊处理,比如日期字段要使用自定义的 SimpleDateFormat,可以参考 AgreementInput.scala 的处理方式。
最后还有一个很现实的判断:Scalatra 官方文档覆盖的问题并不算全面,而且示例质量参差不齐。遇到文档里没写清、示例又对不上的情况,直接去 Stack Overflow 和 GitHub 结合具体报错或代码片段检索,往往比反复猜配置更快。对于 Scalatra 这类相对小众的技术栈,这两个社区仍然是最容易找到完整讨论的地方。













