Scalatra 返回 JSON 时最容易踩的几个坑,顺手把正确写法梳理清楚

蓝鲸
蓝鲸
发布时间:2026-08-25 | 8人浏览过
原创

前言

在 Scalatra 里输出 JSON,表面上只是补几行配置,实际最容易卡住的却是“为什么没报错但还是原样返回”。这篇文章把常见场景按排错顺序重新梳理:先跑通基础配置,再区分普通类、动态字段和 JSONP 的处理方式,最后给出更适合项目落地的组织建议,方便你快速判断每种写法该用在什么地方。

在 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 生效链路排查把依赖、Mixin、jsonFormats 和 contentType 放到同一条链路里。

先检查 scalatra-json 是否真的引入

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

展示 case class、普通类、JObject 三种返回方式差异的对比信息图
三种常见返回策略怎么选当返回对象类型不同,最省事的做法也不同:能自动序列化的不必手工处理,不能自动序列化的则直接显式。

再检查 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 这类相对小众的技术栈,这两个社区仍然是最容易找到完整讨论的地方。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表