1. 概述
有时候,直接将 Java 对象的所有字段一对一地序列化成 JSON 并不合适,甚至不是我们想要的结果。我们可能希望输出一个简化版或增强版的数据视图。这时,自定义 Jackson 序列化器就派上用场了。
但问题来了:如果对象结构复杂,包含大量字段、集合或嵌套对象,手写整个序列化逻辑会非常繁琐。好在 Jackson 提供了一些机制,让我们可以在自定义逻辑中复用默认的序列化能力。
本文将带你了解如何在自定义序列化器中调用 Jackson 的默认序列化器,避免重复造轮子,同时解决一个常见的递归陷阱。
2. 示例数据模型
先来看我们要处理的数据模型。首先是 Folder
类:
public class Folder {
private Long id;
private String name;
private String owner;
private Date created;
private Date modified;
private Date lastAccess;
private List<File> files = new ArrayList<>();
// standard getters and setters
}
以及作为 Folder
成员的 File
类:
public class File {
private Long id;
private String name;
// standard getters and setters
}
我们的目标是:序列化 Folder
时,只保留 name
和 files
,其他字段要么省略,要么单独处理。
3. Jackson 自定义序列化器
使用自定义序列化器的最大好处是:✅ 无需修改原有类结构,同时能将序列化逻辑与业务类解耦。
假设我们期望的 JSON 输出如下:
{
"name": "Root Folder",
"files": [
{"id": 1, "name": "File 1"},
{"id": 2, "name": "File 2"}
]
}
接下来,我们看看几种实现方式。
3.1. 纯手写(暴力实现)
最直接的方式是完全手动控制序列化过程,不依赖任何默认行为。
public class FolderJsonSerializer extends StdSerializer<Folder> {
public FolderJsonSerializer() {
super(Folder.class);
}
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
gen.writeArrayFieldStart("files");
for (File file : value.getFiles()) {
gen.writeStartObject();
gen.writeNumberField("id", file.getId());
gen.writeStringField("name", file.getName());
gen.writeEndObject();
}
gen.writeEndArray();
gen.writeEndObject();
}
}
✅ 优点:完全可控
❌ 缺点:代码冗长,容易出错,维护成本高 —— 特别是当 File
结构变复杂时。
3.2. 复用 ObjectMapper(字符串拼接)
我们可以借助 JsonGenerator
内置的 ObjectMapper
来序列化子对象,简单粗暴:
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
ObjectMapper mapper = (ObjectMapper) gen.getCodec();
gen.writeFieldName("files");
String stringValue = mapper.writeValueAsString(value.getFiles());
gen.writeRawValue(stringValue);
gen.writeEndObject();
}
⚠️ 踩坑提示:这种方式虽然能跑通,但存在性能问题 —— 先序列化成字符串,再写入输出流,多了一次中间转换,不推荐在高性能场景使用。
3.3. 使用 SerializerProvider(推荐)
更优雅的方式是通过 SerializerProvider
调用默认序列化逻辑:
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
gen.writeEndObject();
}
✅ 优点:
- 直接使用 Jackson 内部的序列化流程
- 高效,无中间字符串转换
- 代码简洁,可读性强
这才是我们想要的“复用默认序列化器”的正确姿势。
4. 可能遇到的递归问题
现在需求变了:我们想在输出中加一个 details
字段,把 Folder
的完整信息也塞进去,比如对接老系统或第三方服务。
于是我们尝试这样写:
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
// 这里会炸!
provider.defaultSerializeField("details", value, gen);
gen.writeEndObject();
}
❌ 结果:StackOverflowError
!
为什么?
关键点来了:当你为 Folder
注册了自定义序列化器后,Jackson 会用它**覆盖默认的 BeanSerializer
**。
所以当你调用 provider.defaultSerializeField("details", value, gen)
时,SerializerProvider
查找 Folder
类型的序列化器,拿到的还是你这个自定义的 FolderJsonSerializer
,于是无限递归,栈溢出。
⚠️ 本质:自定义序列化器拦截了所有对该类型的序列化请求,包括它自己发起的。
5. 使用 BeanSerializerModifier 解决递归问题
要破局,必须在 Jackson 覆盖之前,先把默认的 BeanSerializer
保存下来,后续用它来序列化 details
字段。
解决方案:使用 BeanSerializerModifier
。
步骤一:改造自定义序列化器
添加一个字段,用于保存原始的默认序列化器:
public class FolderJsonSerializer extends StdSerializer<Folder> {
private final JsonSerializer<Object> defaultSerializer;
public FolderJsonSerializer(JsonSerializer<Object> defaultSerializer) {
super(Folder.class);
this.defaultSerializer = defaultSerializer;
}
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
gen.writeFieldName("details");
defaultSerializer.serialize(value, gen, provider); // 使用保存的默认序列化器
gen.writeEndObject();
}
}
步骤二:实现 BeanSerializerModifier
在 Jackson 创建序列化器的过程中,拦截 Folder
类型,传入默认序列化器:
public class FolderBeanSerializerModifier extends BeanSerializerModifier {
@Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getBeanClass().equals(Folder.class)) {
return new FolderJsonSerializer((JsonSerializer<Object>) serializer);
}
return serializer;
}
}
步骤三:注册模块
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setSerializerModifier(new FolderBeanSerializerModifier());
mapper.registerModule(module);
步骤四:优化输出(可选)
你会发现 details
里还包含 files
字段,但我们已经在外面单独输出了,重复了。
解决:用 @JsonIgnore
忽略 files
字段:
@JsonIgnore
private List<File> files = new ArrayList<>();
最终输出
{
"name": "Root Folder",
"files": [
{"id": 1, "name": "File 1"},
{"id": 2, "name": "File 2"}
],
"details": {
"id": 1,
"name": "Root Folder",
"owner": "root",
"created": 1565203657164,
"modified": 1565203657164,
"lastAccess": 1565203657164
}
}
✅ 完美达成目标:既简化了主结构,又保留了完整信息,且无递归风险。
6. 总结
本文核心解决了两个问题:
✅ 如何在自定义序列化器中复用默认序列化逻辑
推荐使用SerializerProvider.defaultSerializeField()
,高效且简洁。✅ 如何避免自定义序列化器导致的递归调用
使用BeanSerializerModifier
在 Jackson 覆盖前保存默认序列化器,是标准解法。
所有示例代码已托管至 GitHub:https://github.com/yourname/jackson-custom-serializer-demo(mock 地址)
掌握这些技巧后,你就能在保持灵活性的同时,避免掉进 Jackson 的常见坑里。