Skip to content

Deserializing unwrapped value fails if properties are not creator properties #5279

@odrotbohm

Description

@odrotbohm

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

import static org.assertj.core.api.Assertions.*;

import tools.jackson.databind.json.JsonMapper;

import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;

class Jackson3Test {

  @TestFactory
  Stream<DynamicTest> usesFieldForDeserialization() {

    var mapper = new JsonMapper();
    var typeFactory = mapper.getTypeFactory();

    var working = typeFactory.constructParametricType(Wrapper.class, WorkingSample.class);
    var notWorking = typeFactory.constructParametricType(Wrapper.class, Sample.class);

    var types = Stream.of(working, notWorking);

    return DynamicTest.stream(types, "Should deserialize %s."::formatted, it -> {

      Wrapper<? extends ValueLookup> result = mapper.readValue("""
          {
            "sampleValue" : "SampleValue"
          }
          """, it);

      assertThat(result.getWrapped().getSampleValue()).isEqualTo("SampleValue");
    });
  }

  public static class Wrapper<T> {

    private T wrapped;

    @JsonCreator
    public Wrapper(T wrapped) {
      this.wrapped = wrapped;
    }

    @JsonUnwrapped
    public T getWrapped() {
      return wrapped;
    }
  }

  interface ValueLookup {
    String getSampleValue();
  }

  // Does not consume the property as creator property
  public static class Sample implements ValueLookup {

    private String sampleValue;

    public Sample() {}

    @JsonProperty
    public String getSampleValue() {
      return sampleValue;
    }
  }

  // Consumes the property as creator property
  public static class WorkingSample implements ValueLookup {

    private String sampleValue;

    @JsonCreator
    public WorkingSample(@JsonProperty("sampleValue") String sampleValue) {
      this.sampleValue = sampleValue;
    }

    /**
     * @return the sampleValue
     */
    @JsonProperty
    public String getSampleValue() {
      return sampleValue;
    }
  }
}

Exception for the failing case:

tools.jackson.databind.exc.MismatchedInputException: Unexpected end-of-input when trying read value of type `example.three.Jackson3Test$Sample`
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN]
  at tools.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
  at tools.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1821)
  at tools.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1617)
  at tools.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1566)
  at tools.jackson.databind.deser.bean.BeanDeserializer._handleUnexpectedWithin(BeanDeserializer.java:1355)
  at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:542)
  at tools.jackson.databind.deser.bean.BeanDeserializer.deserialize(BeanDeserializer.java:200)
  at tools.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:552)
  at tools.jackson.databind.deser.impl.UnwrappedPropertyHandler.processUnwrappedCreatorProperties(UnwrappedPropertyHandler.java:86)
  at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeUsingPropertyBasedWithUnwrapped(BeanDeserializer.java:1134)
  at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeWithUnwrapped(BeanDeserializer.java:895)
  at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:475)
  at tools.jackson.databind.deser.bean.BeanDeserializer.deserialize(BeanDeserializer.java:200)
  at tools.jackson.databind.deser.DeserializationContextExt.readRootValue(DeserializationContextExt.java:265)
  at tools.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:2688)
  at tools.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:1642)
  at example.three.Jackson3Test.lambda$1(Jackson3Test.java:32)
  at java.base/java.util.Optional.ifPresent(Optional.java:178)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

The crucial difference seems to be whether the wrapped type consumes the properties as creator properties. If it does, the instance creation pulls values from the buffer. If not, for some reason the parser seems to try to advance on the input. This works on the Jackson 2 (switch the import for JsonMapper back to com.fasterxml.…).

Version Information

No response

Reproduction

Expected behavior

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    to-evaluateIssue that has been received but not yet evaluated

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions