You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

接口添加@JsonDeserialize注解引发栈溢出的原因与解决方法

问题:Jackson多态反序列化按字段名区分实现类的递归问题及解决办法

场景描述

我有一个BookId接口,两个实现类ISBN和ASIN都配置了Jackson注解,能独立正常工作。这两个子类通过字段名而非字段值区分——比如{ "isbn": "xxx" }对应ISBN类,{ "asin": "xxx" }对应ASIN类。我希望序列化时不添加任何包装或额外字段,目标JSON结构如下:

{ "ids": [
    { "isbn" : "978-0-596-52306-0" },
    { "asin" : "B07QKFQ7QJ" }
]}

ISBN类的实现(ASIN类类似,仅验证逻辑不同):

public static class ISBN implements BookId {
    final String isbn;

    @JsonCreator
    public ISBN(@JsonProperty("isbn") String isbn) {
        if (!valid(isbn)) throw new IllegalArgumentException("bad isbn syntax");
        this.isbn = isbn;
    }

    boolean valid(String isbn) { return isbn != null && !isbn.isBlank() /* && checksum ok ... */; }
}

尝试的自定义反序列化器及递归问题

我给BookId接口指定了自定义反序列化器,通过判断字段名选择对应实现类,核心代码如下:

@JsonDeserialize(using = BookId.DeserializerDirectViaJackson.class)
public interface BookId {

    class DeserializerDirectViaJackson extends StdDeserializer<BookId> {
        public DeserializerDirectViaJackson() { this(null); }
        public DeserializerDirectViaJackson(final Class<?> vc) { super(vc); }

        @Override
        public BookId deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            final var om = (ObjectMapper) jsonParser.getCodec();
            final var node = (JsonNode) om.readTree(jsonParser);
            if (!node.isObject()) throw new IllegalStateException("expected JSON object");
            if (node.has("isbn")) return om.treeToValue(node, ISBN.class);
            if (node.has("asin")) return om.treeToValue(node, ASIN.class);
            throw new IllegalStateException("expected 'isbn' or 'asin' field");
        }
    }
    // ...
}

但运行时出现无限递归导致栈溢出,栈追踪片段如下:

Exception in thread "main" java.lang.StackOverflowError
    at java.base/java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:788)
    at com.fasterxml.jackson.databind.node.TreeTraversingParser.nextToken(TreeTraversingParser.java:108)
    at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:100)
    at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:3084)
    at com.bakinsbits.bookcatalog.JacksonInterfaceCustomDeserializerMCVE$BookId$DeserializerDirectViaJackson.deserialize(JacksonInterfaceCustomDeserializerMCVE.java:58)
    at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3438)
    at com.bakinsbits.bookcatalog.JacksonInterfaceCustomDeserializerMCVE$BookId$DeserializerDirectViaJackson.deserialize(JacksonInterfaceCustomDeserializerMCVE.java:60)
    // 重复递归调用...

另外我实现了一个手动解析的反序列化器可以正常工作,但缺点是需要自己处理字段解析和验证,只适合简单场景,无法适配复杂业务:

class DeserializerHomegrown extends StdDeserializer<BookId> {
    public DeserializerHomegrown() { this(null); }
    public DeserializerHomegrown(final Class<?> vc) { super(vc); }

    @Override
    public BookId deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        final var om = (ObjectMapper) jsonParser.getCodec();
        final var node = (JsonNode) om.readTree(jsonParser);
        if (!node.isObject()) throw new IllegalStateException("expected JSON object");
        if (node.has("isbn")) return new ISBN(getValueNode(node, "isbn"));
        if (node.has("asin")) return new ASIN(getValueNode(node, "asin"));
        throw new IllegalStateException("expected 'isbn' or 'asin' field");
    }

    String getValueNode(JsonNode node, String fieldName) {
        final var field = node.get(fieldName);
        if (field == null) return null;
        if (!field.isValueNode()) throw new IllegalStateException("%s field is not JSON value".formatted(fieldName));
        return field.asText();
    }
}

原因分析

无限递归的根源是:ISBN和ASIN都实现了BookId接口,而BookId接口上标注了@JsonDeserialize(using = DeserializerDirectViaJackson.class)。当调用om.treeToValue(node, ISBN.class)时,Jackson会检查ISBN的父接口BookId的反序列化配置,发现要使用自定义的DeserializerDirectViaJackson,于是再次调用这个反序列化器的deserialize方法,形成循环调用,最终栈溢出。

解决方案

方案1:用DeserializationContext直接解析,绕过接口反序列化配置

在自定义反序列化器中,不要调用ObjectMapper.treeToValue,而是通过DeserializationContext的readValue方法,配合TreeTraversingParser解析JsonNode,绕过接口的反序列化器配置,直接解析具体实现类:

@Override
public BookId deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
    ObjectMapper om = (ObjectMapper) jsonParser.getCodec();
    JsonNode node = om.readTree(jsonParser);
    if (!node.isObject()) {
        throw new IllegalStateException("expected JSON object");
    }
    if (node.has("isbn")) {
        // 创建针对当前节点的解析器,直接解析为ISBN类
        try (JsonParser nodeParser = node.traverse(om)) {
            nodeParser.setCodec(om);
            return ctx.readValue(nodeParser, ISBN.class);
        }
    }
    if (node.has("asin")) {
        try (JsonParser nodeParser = node.traverse(om)) {
            nodeParser.setCodec(om);
            return ctx.readValue(nodeParser, ASIN.class);
        }
    }
    throw new IllegalStateException("expected 'isbn' or 'asin' field");
}

方案2:给实现类取消接口的反序列化配置

给ISBN和ASIN类添加@JsonDeserialize注解,指定使用默认反序列化器,覆盖接口的全局配置:

@JsonDeserialize(using = JsonDeserializer.None.class) // 取消接口的反序列化器,用默认解析
public static class ISBN implements BookId {
    // ... 原有代码 ...
}

@JsonDeserialize(using = JsonDeserializer.None.class)
public static class ASIN implements BookId {
    // ... 原有代码 ...
}

这样调用om.treeToValue(node, ISBN.class)时,Jackson会使用默认反序列化逻辑,不会触发接口的自定义反序列化器,避免递归。

方案3:适配Jackson多态反序列化的EXISTING_PROPERTY策略

其实常规Jackson多态反序列化也能适配这种场景,利用@JsonTypeInfo的EXISTING_PROPERTY策略,配合自定义反序列化器识别字段存在性:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, 
              include = JsonTypeInfo.As.EXISTING_PROPERTY,
              property = "type") // 占位字段,实际靠字段存在性区分
@JsonSubTypes({
    @JsonSubTypes.Type(value = ISBN.class, name = "isbn"),
    @JsonSubTypes.Type(value = ASIN.class, name = "asin")
})
@JsonDeserialize(using = BookId.BookIdDeserializer.class)
public interface BookId {
    class BookIdDeserializer extends StdDeserializer<BookId> {
        public BookIdDeserializer() { super(BookId.class); }

        @Override
        public BookId deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
            JsonNode node = p.getCodec().readTree(p);
            if (node.has("isbn")) {
                return ctx.readValue(p, ISBN.class);
            } else if (node.has("asin")) {
                return ctx.readValue(p, ASIN.class);
            }
            throw new JsonParseException(p, "Unknown BookId type");
        }
    }
}

这种方式更贴合Jackson的多态设计规范,同时实现按字段名区分类型的需求。


内容的提问来源于stack exchange,提问作者davidbak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 00:05:05