首页 > 解决方案 > @JsonIdentityReference 不识别相等的值

问题描述

我正在尝试序列化一个对象 ( Root),其中包含一些重复的MyObject. 只想存储整个对象,我正在使用@JsonIdentityReference,效果很好。

但是,我意识到如果存在具有不同引用的相同对象,它将生成不可反序列化的对象。我想知道杰克逊是否有配置来改变这种行为,谢谢!

@Value
@AllArgsConstructor
@NoArgsConstructor(force = true)
class Root {
    private List<MyObject> allObjects;
    private Map<String, MyObject> objectMap;
}

@Value
@AllArgsConstructor
@NoArgsConstructor(force = true)
@JsonIdentityReference
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
class MyObject {
    private String id;
    private int value;
}

public class Main {
    public static void main() throws JsonProcessingException {
        // Constructing equal objects
        val obj1 = new MyObject("a", 1);
        val obj2 = new MyObject("a", 1);
        assert obj1.equals(obj2);

        val root = new Root(
                Lists.newArrayList(obj1),
                ImmutableMap.of(
                        "lorem", obj2
                )
        );
        val objectMapper = new ObjectMapper();

        val json = objectMapper.writeValueAsString(root);
        // {"allObjects":[{"id":"a","value":1}],"objectMap":{"lorem":{"id":"a","value":1}}}
        // Note here both obj1 and obj2 are expanded.

        // Exception: Already had POJO for id 
        val deserialized = objectMapper.readValue(json, Root.class);
        assert root.equals(deserialized);
    }
}

我正在使用杰克逊 2.10。

完整的堆栈跟踪:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Already had POJO for id (java.lang.String) [[ObjectId: key=a, type=com.fasterxml.jackson.databind.deser.impl.PropertyBasedObjectIdGenerator, scope=java.lang.Object]] (through reference chain: Root["objectMap"]->java.util.LinkedHashMap["lorem"]->MyObject["id"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:394)
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:353)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1714)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:371)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithObjectId(BeanDeserializerBase.java:1257)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:157)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer._readAndBindStringKeyMap(MapDeserializer.java:527)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:364)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:29)
    at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3205)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3173)
    at Main.main(Main.java:53)
Caused by: java.lang.IllegalStateException: Already had POJO for id (java.lang.String) [[ObjectId: key=a, type=com.fasterxml.jackson.databind.deser.impl.PropertyBasedObjectIdGenerator, scope=java.lang.Object]]
    at com.fasterxml.jackson.annotation.SimpleObjectIdResolver.bindItem(SimpleObjectIdResolver.java:24)
    at com.fasterxml.jackson.databind.deser.impl.ReadableObjectId.bindItem(ReadableObjectId.java:57)
    at com.fasterxml.jackson.databind.deser.impl.ObjectIdValueProperty.deserializeSetAndReturn(ObjectIdValueProperty.java:101)
    at com.fasterxml.jackson.databind.deser.impl.ObjectIdValueProperty.deserializeAndSet(ObjectIdValueProperty.java:83)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:369)
    ... 14 more

标签: jackson

解决方案


正如我之前提到的,此设置仅在 时有效obj1 == obj2,因为具有相同 ID 的两个对象应该是相同的。在这种情况下,第二个对象也会在序列化过程中得到扩展(alwaysAsId = false仅扩展第一个对象)。

但是,如果您想进行此设置并且对序列化没问题,您可以使用自定义解析器进行反序列化,每个键存储一个实例:

@JsonIdentityReference(alwaysAsId = false)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", resolver = CustomScopeResolver.class)
static class MyObject {

    private String id;
    // ...
}

class CustomScopeResolver implements ObjectIdResolver {

    Map<String, MyObject> data = new HashMap<>();

    @Override
    public void bindItem(final IdKey id, final Object pojo) {
        data.put(id.key.toString(), (MyObject) pojo);
    }

    @Override
    public Object resolveId(final IdKey id) {
        return data.get(id.key);
    }

    @Override
    public ObjectIdResolver newForDeserialization(final Object context) {
        return new CustomScopeResolver();
    }

    @Override
    public boolean canUseFor(final ObjectIdResolver resolverType) {
        return false;
    }

}

新编辑:显然,这很容易:只需打开,objectMapper.configure(SerializationFeature.USE_EQUALITY_FOR_OBJECT_ID, true);以便 DefaultSerializerProvider 使用常规 Hashmap 而不是 IdentityHashMap 来管理序列化的 bean。

已弃用:序列化更新:可以通过添加自定义 SerializationProvider 来实现:

class CustomEqualObjectsSerializerProvider extends DefaultSerializerProvider {

    private final Collection<MyObject> data = new HashSet<>();
    private final SerializerProvider src;
    private final SerializationConfig config;
    private final SerializerFactory f;

    public CustomEqualObjectsSerializerProvider(
            final SerializerProvider src,
            final SerializationConfig config,
            final SerializerFactory f) {
        super(src, config, f);
        this.src = src;
        this.config = config;
        this.f = f;
    }

    @Override
    public DefaultSerializerProvider createInstance(final SerializationConfig config, final SerializerFactory jsf) {
        return new CustomEqualObjectsSerializerProvider(src, this.config, f);
    }

    @Override
    public WritableObjectId findObjectId(final Object forPojo, final ObjectIdGenerator<?> generatorType) {
        // check if there is an equivalent pojo, use it if exists
        final Optional<MyObject> equivalentObject = data.stream()
                .filter(forPojo::equals)
                .findFirst();

        if (equivalentObject.isPresent()) {
            return super.findObjectId(equivalentObject.get(), generatorType);
        } else {
            if (forPojo instanceof MyObject) {
                data.add((MyObject) forPojo);
            }
            return super.findObjectId(forPojo, generatorType);
        }

    }

}

@Test
public void main() throws IOException {
    // Constructing equal objects
    final MyObject obj1 = new MyObject();
    obj1.setId("a");

    final MyObject obj2 = new MyObject();
    obj2.setId("a");
    assert obj1.equals(obj2);

    final Root root = new Root();
    root.setAllObjects(Collections.singletonList(obj1));
    root.setObjectMap(Collections.singletonMap(
            "lorem", obj2));
    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setSerializerProvider(
            new CustomEqualObjectsSerializerProvider(
                    objectMapper.getSerializerProvider(),
                    objectMapper.getSerializationConfig(),
                    objectMapper.getSerializerFactory()));
    final String json = objectMapper.writeValueAsString(root);
    System.out.println(json); // second object is not expanded!
}

推荐阅读