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

如何使用Jackson反序列化含type-id引用与links结构的复杂JSON?

如何用Jackson将自定义引用结构的JSON反序列化为目标Java类?

首先明确我们的输入和目标:

原始API返回的JSON

{
  "data": {
    "id": 123,
    "name": "foo",
    "createdBy": { "id": 234, "type": "user" },
    "currency": { "id": 456, "type": "currency" }
  },
  "links": {
    "user": [ { "id": 234, "fullName": "Peter" } ],
    "currency": [ { "id": 456, "symbol": "Euro" } ]
  }
}

目标Java类

public class Foo {
    private int id;
    private String name;
    private User createdBy;
    private Currency currency;
    // 省略getter、setter
}

public class User {
    private int id;
    private String fullName;
    // 省略getter、setter
}

public class Currency {
    private int id;
    private String symbol;
    // 省略getter、setter
}

这个API的引用逻辑确实有点特殊——它不用标准的@idRef这类引用标记,而是用包含id和type的对象指向links里的实体详情。你试过ObjectIdResolver没找到方案很正常,因为Jackson的默认对象引用机制是基于单一id字段的,没法处理这种带type的多类型引用场景。不过我们可以通过几种方式实现需求:


方案一:手动映射(简单直接,适合小场景)

这种方式最容易上手,先把整个JSON解析到临时容器类里,再手动把links里的实体关联到Foo对象上。

步骤1:定义顶层容器类

创建一个能完整接收JSON结构的容器类:

public class ApiResponse {
    private FooData data;
    private Links links;

    // 省略getter、setter

    // 对应data节点的临时类,用Reference接收引用
    public static class FooData {
        private int id;
        private String name;
        private Reference createdBy;
        private Reference currency;
        // 省略getter、setter
    }

    // 接收type+id的引用类
    public static class Reference {
        private int id;
        private String type;
        // 省略getter、setter
    }

    // 对应links节点的类
    public static class Links {
        private List<User> user;
        private List<Currency> currency;
        // 省略getter、setter
    }
}

步骤2:解析并手动关联

解析完整个响应后,把links里的实体存入Map方便查找,再构建最终的Foo对象:

ObjectMapper mapper = new ObjectMapper();
// 先解析整个API响应
ApiResponse response = mapper.readValue(jsonString, ApiResponse.class);

// 把links里的实体按type和id存入Map
Map<String, Map<Integer, Object>> linkMap = new HashMap<>();
linkMap.put("user", response.getLinks().getUser().stream()
        .collect(Collectors.toMap(User::getId, u -> u)));
linkMap.put("currency", response.getLinks().getCurrency().stream()
        .collect(Collectors.toMap(Currency::getId, c -> c)));

// 构建最终的Foo对象
Foo foo = new Foo();
foo.setId(response.getData().getId());
foo.setName(response.getData().getName());
// 根据type和id从linkMap里取出对应的实体
foo.setCreatedBy((User) linkMap.get(response.getData().getCreatedBy().getType())
        .get(response.getData().getCreatedBy().getId()));
foo.setCurrency((Currency) linkMap.get(response.getData().getCurrency().getType())
        .get(response.getData().getCurrency().getId()));

这种方式没有复杂的自定义逻辑,代码可读性高,适合结构不复杂、不需要频繁复用的场景。


方案二:自定义反序列化器(自动化程度高)

如果想让Jackson自动完成关联,可以自定义一个上下文感知的反序列化器,让它在解析Foo的引用字段时,自动从links里查找对应的实体。

步骤1:定义标记注解

创建一个注解,用来标记哪些字段需要自动解析引用:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResolveReference {
}

步骤2:实现上下文感知的反序列化器

这个反序列化器会根据字段的目标类型(比如User或Currency),从预先缓存的links数据里查找对应的实体:

public class ReferenceDeserializer extends StdDeserializer<Object> implements ContextualDeserializer {
    private Class<?> targetType;

    public ReferenceDeserializer() {
        super(Object.class);
    }

    public ReferenceDeserializer(Class<?> targetType) {
        super(targetType);
        this.targetType = targetType;
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        // 先解析出type+id的引用对象
        Reference ref = p.readValueAs(Reference.class);
        // 从ThreadLocal缓存里取出之前准备好的linkMap
        Map<String, Map<Integer, Object>> linkMap = LinkCache.get();
        return linkMap.get(ref.getType()).get(ref.getId());
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        // 获取当前字段的目标类型(比如User、Currency)
        targetType = property.getType().getRawClass();
        return new ReferenceDeserializer(targetType);
    }
}

步骤3:创建ThreadLocal缓存

用来临时存储links数据,让反序列化器能拿到:

public class LinkCache {
    private static final ThreadLocal<Map<String, Map<Integer, Object>>> cache = new ThreadLocal<>();

    public static void set(Map<String, Map<Integer, Object>> linkMap) {
        cache.set(linkMap);
    }

    public static Map<String, Map<Integer, Object>> get() {
        return cache.get();
    }

    public static void clear() {
        cache.remove();
    }
}

步骤4:修改Foo类标记字段

给需要解析引用的字段加上注解和反序列化器:

public class Foo {
    private int id;
    private String name;

    @ResolveReference
    @JsonDeserialize(using = ReferenceDeserializer.class)
    private User createdBy;

    @ResolveReference
    @JsonDeserialize(using = ReferenceDeserializer.class)
    private Currency currency;
    // 省略getter、setter
}

步骤5:执行解析

先解析整个响应拿到links数据,存入缓存后再解析data为Foo对象:

ObjectMapper mapper = new ObjectMapper();
ApiResponse response = mapper.readValue(jsonString, ApiResponse.class);

// 构建linkMap并存入缓存
Map<String, Map<Integer, Object>> linkMap = new HashMap<>();
linkMap.put("user", response.getLinks().getUser().stream()
        .collect(Collectors.toMap(User::getId, u -> u)));
linkMap.put("currency", response.getLinks().getCurrency().stream()
        .collect(Collectors.toMap(Currency::getId, c -> c)));
LinkCache.set(linkMap);

// 解析data部分为Foo对象,此时反序列化器会自动关联实体
Foo foo = mapper.readValue(mapper.writeValueAsString(response.getData()), Foo.class);

// 记得清理缓存,避免线程污染
LinkCache.clear();

这种方式自动化程度高,适合需要多次解析、结构复杂的场景。


方案三:使用Jackson的InjectableValues(线程更安全)

如果不想用ThreadLocal,可以用Jackson的InjectableValues把linkMap直接注入到反序列化器中,这样更线程安全。

步骤1:修改反序列化器支持注入

调整ReferenceDeserializer,让它从上下文获取注入的linkMap:

public class ReferenceDeserializer extends StdDeserializer<Object> implements ContextualDeserializer {
    private Class<?> targetType;
    private Map<String, Map<Integer, Object>> linkMap;

    public ReferenceDeserializer() {
        super(Object.class);
    }

    public ReferenceDeserializer(Class<?> targetType, Map<String, Map<Integer, Object>> linkMap) {
        super(targetType);
        this.targetType = targetType;
        this.linkMap = linkMap;
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        Reference ref = p.readValueAs(Reference.class);
        return linkMap.get(ref.getType()).get(ref.getId());
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        targetType = property.getType().getRawClass();
        // 从上下文获取注入的linkMap
        Map<String, Map<Integer, Object>> injectedLinkMap = (Map<String, Map<Integer, Object>>) ctxt.findInjectableValue("linkMap", null, null);
        return new ReferenceDeserializer(targetType, injectedLinkMap);
    }
}

步骤2:执行解析

设置InjectableValues后再解析Foo对象:

ObjectMapper mapper = new ObjectMapper();
ApiResponse response = mapper.readValue(jsonString, ApiResponse.class);

// 构建linkMap
Map<String, Map<Integer, Object>> linkMap = new HashMap<>();
linkMap.put("user", response.getLinks().getUser().stream()
        .collect(Collectors.toMap(User::getId, u -> u)));
linkMap.put("currency", response.getLinks().getCurrency().stream()
        .collect(Collectors.toMap(Currency::getId, c -> c)));

// 把linkMap注入到Jackson的解析上下文中
InjectableValues inject = new InjectableValues.Std().addValue("linkMap", linkMap);
Foo foo = mapper.reader(inject).readValue(mapper.writeValueAsString(response.getData()), Foo.class);

这种方式不需要ThreadLocal,避免了线程污染的风险,适合多线程环境下使用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:46:25