jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

No way to combine `@JsonTypeInfo(include = As.PROPERTY)` and `@JsonIdentityInfo(generator = PropertyGenerator.class)`

Open knutwannheden opened this issue 11 months ago • 3 comments

Describe the bug I have tried many different ways to combine @JsonTypeInfo(include = As.PROPERTY) and @JsonIdentityInfo(generator = PropertyGenerator.class), but it always ends up throwing an exception at me like this one:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid Object Id definition for `foo.JacksonDeserializationTest$BaseEntity`: cannot find property with name '@id'
 at [Source: (String)"{"@c":"foo.JacksonDeserializationTest$Bar","@id":1,"foo":{"@c":"foo.JacksonDeserializationTest$Foo","@id":0,"other":1}}"; line: 1, column: 1]

	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1915)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:268)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
	at com.fasterxml.jackson.databind.DeserializationContext.findNonContextualValueDeserializer(DeserializationContext.java:644)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:539)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:294)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
	at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:654)
	at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4956)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4826)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3772)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3740)

Version information 2.15.2

To Reproduce

class JacksonDeserializationTest {

    @Test
    void test() throws Exception {
        Foo foo = new Foo();
        Bar bar = new Bar();
        foo.setOther(bar);
        bar.setFoo(foo);

        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(bar));
        Bar deserialized = mapper.readValue(mapper.writeValueAsString(bar), Bar.class);
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@c")
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "@id")
    public interface BaseEntity {
        class Id {
            static int next = 0;
        }
        @JsonProperty("@id")
        Integer getId();
    }

    public static class Foo implements BaseEntity {
        private BaseEntity other;
        private Integer id = Id.next++;

        @Override
        public Integer getId() {
            return id;
        }

        public BaseEntity getOther() {
            return other;
        }

        public void setOther(BaseEntity other) {
            this.other = other;
        }
    }

    public static class Bar implements BaseEntity {
        private BaseEntity foo;

        private Integer id = Id.next++;

        @Override
        public Integer getId() {
            return id;
        }

        public BaseEntity getFoo() {
            return foo;
        }

        public void setFoo(BaseEntity foo) {
            this.foo = foo;
        }
    }
}

Expected behavior It seems like it should be possible to combine these two features somehow. I have tried all combinations I can think of, but couldn't get it to work, so I assume there must be some bug here.

Additional context I have a set of Java objects I want to serialize, where the references in the object graph form many cycles, so I need to use @JsonIdentityInfo for this. Also, I have an inheritance hierarchy, so I need to use @JsonTypeInfo to cover this.

knutwannheden avatar Jul 06 '23 18:07 knutwannheden

That does sound like a reasonable use case.

I suspect the issue might be the lack of setter for "@id" -- you could add a bogus @JsonProperty("@id") public void setId() { } if you don't want assignment?

cowtowncoder avatar Jul 06 '23 21:07 cowtowncoder

Thanks for the suggestion. Let me try that and report back.

Meanwhile, I was able to get it to work if I let Jackson generate the IDs, which is also acceptable, even if the ID is redundant.

As an aside: I was also getting strange errors when my classes participating in the cycles didn't have an @JsonCreator annotated default constructor. Of course it makes sense that the properties corresponding to the edges in the cycle cannot be part of the object constructor used by Jackson, but I had it this way, because when I manually instantiate the object, I pass null values for these properties and then later update them with a setter. It is just that the error message had me puzzled as to what the problem could be.

I would also like to mention how extremely satisfied I am with Jackson! It has a great feature set, well thought-out APIs, well commented code, is very fast and memory-efficient, and not to forget the excellent community support! Thanks a lot for this huge effort 🙏

knutwannheden avatar Jul 07 '23 05:07 knutwannheden

I have the same issue when using Constructor-based instantiation (no setters, no mutability). Adding a bogus setter makes it work, but setters should not be part of my interface.

iTitus avatar Feb 08 '24 19:02 iTitus