Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserialization fails depending on the order of deserialized objects with "Cannot construct instance (although at least one Creator exists)" #3355

Open
dinopraso opened this issue Dec 21, 2021 · 11 comments
Labels
has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue

Comments

@dinopraso
Copy link

dinopraso commented Dec 21, 2021

Describe the bug
Deserialization of a specific class becomes broken if another specific class is deserialized before it. All classes are immutable and depend on @JsonCreator and property-based constructors with all properties annotated correctly with @JsonProperty.

If a class ContainerOne is deserialized before a class ContainerTwo (see below for reference), deserialization will fail, but it you flip their order, it will work as expected.

It appears that some sort of caching is handled incorrectly making the second invocation ignore the property-based creator.

Version information
Reproduced on 2.12.3 and 2.13.0

To Reproduce
Here is the minimal setup I've managed to make to reproduce this.

public class Common {
    private final String property;
    private final List<ContainerTwo> twos;

    @JsonCreator
    public Common(@JsonProperty("property") final String property,
                  @JsonProperty("twos") final List<ContainerTwo> twos) {
        this.property = property;
        this.twos = twos;
    }

    public String getProperty() {
        return property;
    }

    // @JsonIgnoreProperties("common") // This is commented out because it does not contribute to the reproduction, but would be present for such a setup.
    public List<ContainerTwo> getTwos() {
        return twos;
    }
}

public class ContainerOne {
    private final Common common;

    @JsonCreator
    public ContainerOne(@JsonProperty("common") final Common common) {
        this.common = common;
    }

    public Common getCommon() {
        return common;
    }
}

public class ContainerTwo {
    private final Common common;

    @JsonCreator
    public ContainerTwo(@JsonProperty("common") final Common common) {
        this.common = common;
    }

    @JsonIgnoreProperties("twos")
    public Common getCommon() {
        return common;
    }
}

public static void main(final String[] args) throws Exception {
    final String oneJson = "{ \"common\": { \"property\": \"valueOne\" } }";
    final String twoJson = "{ \"common\": { \"property\": \"valueTwo\" } }";

    final ObjectMapper objectMapper = new ObjectMapper();

    final ContainerOne one = objectMapper.readValue(oneJson, ContainerOne.class);
    final ContainerTwo two = objectMapper.readValue(twoJson, ContainerTwo.class); // This fails, unless invoked before the line directly above.
}

I have found two ways to stop this from happening, both of which are not really an option for me at this point:

  1. Remove @JsonIgnoreProperties("twos") from the getter in ContainerTwo
  2. Make Common mutable, with setters

Expected behavior
Since this behavior is specific to the execution order and breaks seemingly unexpectedly, it appears to be a bug.
Expected behavior should be that both objects are deserialized (like they are individually) regardless of their invocation order.

Additional Information
Here is the full stack trace of the exception that happens

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.dinopraso.serialization.json.Common` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{ "common": { "property": "valueTwo" } }"; line: 1, column: 15] (through reference chain: com.dinopraso.serialization.json.ContainerTwo["common"])
	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
	at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1728)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1353)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:351)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:184)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:542)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:563)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:438)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1405)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:351)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:184)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4675)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3630)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3598)
@dinopraso dinopraso added the to-evaluate Issue that has been received but not yet evaluated label Dec 21, 2021
@cowtowncoder
Copy link
Member

It does sound like a bug, and my first guess would be it might be related to @JsonIgnoreProperties handling.

@cowtowncoder cowtowncoder added 2.13 has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue and removed to-evaluate Issue that has been received but not yet evaluated labels Dec 21, 2021
@dinopraso
Copy link
Author

dinopraso commented Dec 22, 2021

After some more fiddling around, I've noticed that that ContainerOne is not necessary for the reproduction of the issue. Here is an even simpler example (might help in locating the issue easier)

public class Common {
    private final String property;
    private final Container container;

    @JsonCreator
    public Common(@JsonProperty("property") final String property,
                  @JsonProperty("container") final Container container) {
        this.property = property;
        this.container = container;
    }

    public String getProperty() {
        return property;
    }

    public Container getContainer() {
        return container;
    }
}

public class Container {
    private final Common common;

    @JsonCreator
    public Container(@JsonProperty("common") final Common common) {
        this.common = common;
    }

    @JsonIgnoreProperties("container")
    public Common getCommon() {
        return common;
    }
}

public static void main(final String[] args) throws Exception {
    final String objectJson = "{ \"property\": \"valueOne\" }";
    final String containersJson = "{ \"common\": { \"property\": \"valueTwo\" } }";

    final ObjectMapper objectMapper = new ObjectMapper();

    final Common object = objectMapper.readValue(objectJson, Common.class);
    final Container container = objectMapper.readValue(containersJson, Container.class); // Fails
}

Also, it seems to be only an issue with the property-based creator. Adding any other creator (setters or POJOBuilder) makes the issue go away, though both are not ideal workarounds.

EDIT: Updated to further simplify the case. Common does not have to have a collection of Container, it can be a plain field.

@dinopraso
Copy link
Author

dinopraso commented Jan 17, 2022

Can we get any updates on this? It's fairly important for us to have this fixed soon, and we would like to avoid having to write temporary workarounds in our applications.

@cowtowncoder
Copy link
Member

@dinopraso Unfortunately I don't have time to work on this at this point in time. I hope sometimes else can help, or you figure it out.

@dinopraso
Copy link
Author

I've been looking though the source code for a while, and I've figured something out! This whole time, I've have the assumption that, since it is only the getter which was annotated with @JsonIgnoreProperties, it should only ever affect serialisation, never deserialisation. However, setting @JsonIgnoreProperties(allowSetters = true) resolves this issue completely.

I am not sure whether this behaviour is expected thought? Should @JsonIgnoreProperties on a getter really affect deserialisation?

Either way, at least we now have a simple way to fix this in our projects.

@yawkat
Copy link
Member

yawkat commented Jan 20, 2022

Yes this is also the behavior for other annotations, the best example being @JsonProperty:

class Test {
    private String foo;

    @JsonProperty("bar")
    public String getFoo() {
        return foo;
    }

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

The property will be called bar for both serialization and deserialization.

You can essentially think of annotations as applying to the property (getter + setter), not the individual accessor, though there are some special rules when it comes to conflicting annotations.

@dinopraso
Copy link
Author

dinopraso commented Jan 20, 2022

Though the issue still remains to be fixed, since the fact that a set of properties was ignored on one class shouldn't affect the deserialization of another

@cowtowncoder
Copy link
Member

The behavior about "unifying annotations" (combining) -- that annotations in one accessor are indeed applied to the "whole" logical property, regardless of which accessor had it -- is indeed 100% intended and working as defined.
The reason for this is to reduce the need to apply multiple duplicate annotations for things like renaming (as illustrated by @yawkat's example).
The process of unification is applied for all Jackson annotations without explicit rules in trying to determine separate semantics for separate annotations.
But even if handling were separated, I would argue that @JsonIgnoreProperties should apply regardless of setter/getter used to denote it.

Having said that it sounds like there is a separate issue which is likely due to the way that @JsonIgnoreProperties is applied via reference. It sounds likely that the "modified" instance (with ignoral) gets cached and incorrectly used for non-annotated case.
I think there is another existing issue filed for recursive use case that is probably related.

@pparam5241
Copy link

In order to fix this issue, You may create your JsonDeserializer which will help you to convert this.

I've used below snippet in order to fix my issue which works fine.

public class JsonStringDeserializer extends JsonDeserializer<Value> {

    @Override
    public Value deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
        ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
        String jsonString = jsonParser.getText();
        return mapper.readValue(jsonString, Value.class);
    }
}

Here, Value is my class.

@cowtowncoder
Copy link
Member

If anyone has time, would be good to see if this still occurs with 2.18.2 -- good chance it's been fixed.

JooHyukKim added a commit to JooHyukKim/jackson-databind that referenced this issue Dec 5, 2024
@JooHyukKim
Copy link
Member

Yes, still failing in 2.18 latest version.
Reproduced via #4833.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue
Projects
None yet
Development

No branches or pull requests

6 participants
@cowtowncoder @yawkat @dinopraso @pparam5241 @JooHyukKim and others