Issue with order of inheritance type attribute

230 views
Skip to first unread message

Michael Schäfer

unread,
Mar 2, 2022, 10:47:49 AM3/2/22
to jackson-user
Hi guys,

I'm currently facing an odd behavior with how polymorphic XML objects are deserialized when using Jackson 2.13.1. The deserialization fails when the type attribute is not the first attribute in the setting tag. This seems odd, as Jackson can even properly determine the correct type. My issue with this case is that my code has to handle XML documents that are generated by a source I don't have control over and it sadly generates XML documents where the type attribute is put last. So for example the following XML works:

<setting type="localizedString" key="testKey">
        <value locale="default">testValue</value>
        <value locale="de-DE">testValue deutsch</value>
</setting>


and the following XML fails:

<setting key="testKey" type="localizedString">
        <value locale="default">testValue</value>
        <value locale="de-DE">testValue deutsch</value>
</setting>

with the following error:
com.fasterxml.jackson.databind.JsonMappingException: Unexpected non-whitespace text ('testValue' in Array context: should not occur (or should be handled)
 at [Source: (StringReader); line: 2, column: 42] (through reference chain: ch.michael.experimental.JacksonInheritanceOrder$LocalizedSetting["value"])
        at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:392)
        at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:351)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1821)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:315)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:214)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:186)
        at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:122)
        at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:144)
        at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:110)
        at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
        at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
        at com.fasterxml.jackson.dataformat.xml.deser.XmlDeserializationContext.readRootValue(XmlDeserializationContext.java:91)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3629)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3597)
        at ch.michael.experimental.JacksonInheritanceOrder.executeTest(JacksonInheritanceOrder.java:143)
        at ch.michael.experimental.JacksonInheritanceOrder.deserializeTrailingType(JacksonInheritanceOrder.java:128)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
        at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
        at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
        at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
        at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
        at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
        at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:93)
        at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected non-whitespace text ('testValue' in Array context: should not occur (or should be handled)
 at [Source: (StringReader); line: 2, column: 42]
        at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2391)
        at com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser.nextToken(FromXmlParser.java:847)
        at com.fasterxml.jackson.core.util.JsonParserSequence.nextToken(JsonParserSequence.java:151)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:346)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
        at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:313)
        ... 43 more

 
For demo purposes I have create the following little demo test:

import static org.junit.Assert.assertEquals;

import java.util.LinkedList;
import java.util.List;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;

public class JacksonInheritanceOrder {
 
  @JsonInclude(JsonInclude.Include.NON_NULL)
  @JacksonXmlRootElement(localName = "setting")
  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = Setting.ATTRIBUTE_TYPE)
  @JsonSubTypes({@Type(value = LocalizedSetting.class, name = LocalizedSetting.TYPE)})
  public static interface Setting {

    public static final String ATTRIBUTE_TYPE = "type";

    @JacksonXmlProperty(localName = "key", isAttribute = true)
    public String getKey();

    public void setKey(String key);
   
    @JacksonXmlProperty(localName = ATTRIBUTE_TYPE, isAttribute = true)
    public String getType();
  }
 
  public static class LocalizedSetting implements Setting {

    public static final String TYPE = "localizedString";

    @JacksonXmlProperty(localName = "key", isAttribute = true)
    private String key;

    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty(localName = "value")
    private List<LocalizedValue> localizedValues = new LinkedList<>();

    @Override
    public String getKey() {
      return key;
    }

    @Override
    public void setKey(String key) {
      this.key = key;
    }
   
    public List<LocalizedValue> getLocalizedValues() {
      return localizedValues;
    }
   
    public void setLocalizedValues(List<LocalizedValue> values) {
      this.localizedValues = values;
    }
   
    @Override
    public String getType() {
      return TYPE;
    }
  }
 
  @JacksonXmlRootElement(localName = "value")
  public static class LocalizedValue {

    @JacksonXmlProperty(localName = "locale", isAttribute = true)
    private String locale;
   
    @JacksonXmlText
    private String value;

    public String getLocale() {
      return locale;
    }

    public void setLocale(String locale) {
      this.locale = locale;
    }

    public String getValue() {
      return value;
    }

    public void setValue(String value) {
      this.value = value;
    }
  }
 
  @BeforeClass
  public static void setUpBeforeClass() throws Exception {}

  @AfterClass
  public static void tearDownAfterClass() throws Exception {}

  @Before
  public void setUp() throws Exception {}

  @After
  public void tearDown() throws Exception {}
 
  private static final String XML_TMPL = "<setting%s key=\"testKey\"%s>\n"
      + "<value locale=\"default\">testValue</value>\n"
      + "<value locale=\"de-DE\">testValue deutsch</value>\n"
      + "</setting>";
 
  /**
   * Fails with JsonMappingException?!
   *
   * @throws Exception
   */
  @Test
  public void deserializeTrailingType() throws Exception {
    executeTest(String.format(XML_TMPL, "", " type=\"localizedString\""));
  }
 
  /**
   * Works as expected.
   *
   * @throws Exception
   */
  @Test
  public void deserializeLeadingType() throws Exception {
    executeTest(String.format(XML_TMPL, " type=\"localizedString\"", ""));
  }
 
  private void executeTest(String xmlInput) throws Exception {
    XmlMapper xmlMapper = new XmlMapper();
    Setting setting = xmlMapper.readValue(xmlInput, Setting.class);
    assertEquals("testKey", setting.getKey());
   
    assertEquals(LocalizedSetting.class, setting.getClass());
    List<LocalizedValue> values = ((LocalizedSetting)setting).getLocalizedValues();
    assertEquals(2, values.size());
    assertEquals("default", values.get(0).getLocale());
    assertEquals("testValue", values.get(0).getValue());
    assertEquals("de-DE", values.get(1).getLocale());
    assertEquals("testValue deutsch", values.get(1).getValue());
  }
}


Is this a bug or known issue? Can I work around it?

Michael Schäfer

unread,
Mar 14, 2022, 4:34:29 AM3/14/22
to jackson-user
Hi Again,

can really nobody help me? Is important information missing from my request? And off course this is a stripped version of my code to reproduce the case. I really need the inheritance. To me this also looks like the final road block. It would be really nice when I could get some feedback from a more experienced dev.

Regards,
Michael

Tatu Saloranta

unread,
Mar 15, 2022, 4:15:02 PM3/15/22
to jackso...@googlegroups.com
On Mon, Mar 14, 2022 at 1:34 AM Michael Schäfer <msk...@gmail.com> wrote:
>
> Hi Again,
>
> can really nobody help me? Is important information missing from my request? And off course this is a stripped version of my code to reproduce the case. I really need the inheritance. To me this also looks like the final road block. It would be really nice when I could get some feedback from a more experienced dev.

I can only speculate that this is a limitation of the XML module, and
sounds like a bug.
I suggest you file an issue against `jackson-dataformat-xml`.

Unfortunately handling of `@JsonTypeInfo` in the XML module is tricky:
the problem may be due to buffering needed.

On issue it would be good to have a minimal (smallest possible)
reproduction of the problem. I realize that example is already
stripped down version but anything can help.

-+ Tatu +-
> --
> You received this message because you are subscribed to the Google Groups "jackson-user" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to jackson-user...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/jackson-user/0afadee1-5b12-4b33-874c-d4f2632a511cn%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages