如何使用Jackson反序列化含type-id引用与links结构的复杂JSON?
首先明确我们的输入和目标:
原始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

