Jackson serialization XML with root to abstract class

Viewed 602

I have an application and I need to serialize XML with different tags at the root level.

I implemented a test case with the following scenario, through the same input I get both xmls.

<ClassTypeA>
    <fieldA>a</fieldA>
</ClassTypeA>

and

<ClassTypeB>
    <fieldB>b</fieldB>
</ClassTypeB>

To serialize I implemented these three classes:


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonSubTypes({
        @JsonSubTypes.Type(value = TestConcreteDisconnectXML.class, name = "ClassTypeA"),
        @JsonSubTypes.Type(value = TestConcreteConnectXML.class, name = "ClassTypeB"),
})
abstract class TestXml{

}

@Getter
@Setter
class TestConcreteDisconnectXML extends TestXml{
    private String fieldA;
}

@Getter
@Setter
class TestConcreteConnectXML extends TestXml{
    private String fieldB;
}

using this test below the following error is returned:

    public void testDeserializeXML() throws Exception {

        objectMapper = new XmlMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        
        
        String message = "<ClassTypeA>\n" +
                "\t<fieldA>a</fieldA>\n" +
                "</ClassTypeA>";

        TestXml cxml =   this.objectMapper.readValue(message, TestXml.class);

        message = "<ClassTypeB>\n" +
                "\t<fieldB>b</fieldB>\n" +
                "</ClassTypeB>";

        cxml =   this.objectMapper.readValue(message, TestXml.class);


    }
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'fieldA' as a subtype of `br.com.eletra.filemode.uaa.converter.TestXml`: known type ids = [ClassTypeA, ClassTypeB]
 at [Source: (StringReader); line: 2, column: 2]

However, when inserting any tag at the beginning of the structure, serialization is successful.

enter image description here

I`ve made another test with the jackson 2.12 with deduction type.

@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
        @JsonSubTypes.Type(ClassTypeA.class),
        @JsonSubTypes.Type(ClassTypeB.class),
        @JsonSubTypes.Type(DefaultClass.class),
})
abstract class TestXml{

}

@NoArgsConstructor
@Getter
@Setter
class ClassTypeA extends TestXml{
    private String fieldA;
}

@NoArgsConstructor
@Getter
@Setter
class ClassTypeB extends TestXml{
    private String fieldB;
}

@NoArgsConstructor
@Getter
@Setter
class DefaultClass extends TestXml{

}

but a null error without details was thrown:

java.lang.NullPointerException
    at com.fasterxml.jackson.dataformat.xml.util.StaxUtil.sanitizeXmlTypeName(StaxUtil.java:81)
    at com.fasterxml.jackson.dataformat.xml.XmlTypeResolverBuilder.typeProperty(XmlTypeResolverBuilder.java:45)
    at com.fasterxml.jackson.dataformat.xml.XmlTypeResolverBuilder.typeProperty(XmlTypeResolverBuilder.java:25)
    at com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector._findTypeResolver(JacksonAnnotationIntrospector.java:1517)
    at com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector.findTypeResolver(JacksonAnnotationIntrospector.java:572)
    at com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair.findTypeResolver(AnnotationIntrospectorPair.java:236)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findTypeDeserializer(BasicDeserializerFactory.java:1754)
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:597)
    at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4731)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4592)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3546)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3514)
    at br.com.eletra.filemode.uaa.converter.XmlConvertTest.testDeserializeXML(XmlConvertTest.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at junit.framework.TestCase.runTest(TestCase.java:177)
    at junit.framework.TestCase.runBare(TestCase.java:142)
    at junit.framework.TestResult$1.protect(TestResult.java:122)
    at junit.framework.TestResult.runProtected(TestResult.java:142)
    at junit.framework.TestResult.run(TestResult.java:125)
    at junit.framework.TestCase.run(TestCase.java:130)
    at junit.framework.TestSuite.runTest(TestSuite.java:241)
    at junit.framework.TestSuite.run(TestSuite.java:236)
    at org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:90)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)


Process finished with exit code -1

Any suggestion to implement something like this, without insert a tag into the runtime?

3 Answers

You might try to use DEDUCTION to find the subtype, and so the Root tag name is irrelevant, the only thing is used is the fields:

@JsonTypeInfo(use=Id.DEDUCTION, defaultImpl = DefaultClassToUse.class)
... 

For me it's working something like this:

public void testDeserializeXML() throws Exception {

        objectMapper = new XmlMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        
        
        String message = "<ClassTypeA>\n" +
                "\t<fieldA>a</fieldA>\n" +
                "</ClassTypeA>";

        TestXml cxml =   this.objectMapper.readValue(message, TestXml.class);

        message = "<ClassTypeB>\n" +
                "\t<fieldB>b</fieldB>\n" +
                "</ClassTypeB>";

        cxml =   this.objectMapper.readValue(message, TestXml.class);


    }

with

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonSubTypes({
        @JsonSubTypes.Type(value = TestConcreteDisconnectXML.class, name = "fieldA"),
        @JsonSubTypes.Type(value = TestConcreteConnectXML.class, name = "fieldB")
})
abstract class TestXml {

}

@Getter
@Setter
@AllArgsConstructor
class TestConcreteDisconnectXML extends TestXml{
    private String fieldA;
}

@Getter
@Setter
@AllArgsConstructor
class TestConcreteConnectXML extends TestXml {
    private String fieldB;
}

The problem is that xml has no root. If you can add root to xml, then you will be able to use this:

public void testDeserializeXML() throws Exception {

        objectMapper = new XmlMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        
        
        String message = "<root><ClassTypeA>\n" +
                "\t<fieldA>a</fieldA>\n" +
                "</ClassTypeA></root>";

        TestXml cxml =   this.objectMapper.readValue(message, TestXml.class);

        message = "<root><ClassTypeB>\n" +
                "\t<fieldB>b</fieldB>\n" +
                "</ClassTypeB></root>";

        cxml =   this.objectMapper.readValue(message, TestXml.class);


    }
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonSubTypes({
        @JsonSubTypes.Type(value = TestConcreteDisconnectXML.class, name = "ClassTypeA"),
        @JsonSubTypes.Type(value = TestConcreteConnectXML.class, name = "ClassTypeB")
})
abstract class TestXml {

}

@Getter
@Setter
class TestConcreteConnectXML extends TestXml {
    private String fieldB;
}

@Getter
@Setter
class TestConcreteDisconnectXML extends TestXml{
    private String fieldA;
}

I don't know any solution to have this behavior without root element when using XmlMapper. If you want, you could try to use other XML libraries which are dedicated to XML to achieve this without root element.

Related