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 时,只保留 namefiles,其他字段要么省略,要么单独处理。


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. 总结

本文核心解决了两个问题:

  1. 如何在自定义序列化器中复用默认序列化逻辑
    推荐使用 SerializerProvider.defaultSerializeField(),高效且简洁。

  2. 如何避免自定义序列化器导致的递归调用
    使用 BeanSerializerModifier 在 Jackson 覆盖前保存默认序列化器,是标准解法。

所有示例代码已托管至 GitHub:https://github.com/yourname/jackson-custom-serializer-demo(mock 地址)

掌握这些技巧后,你就能在保持灵活性的同时,避免掉进 Jackson 的常见坑里。


原始标题:Calling Default Serializer from Custom Serializer in Jackson