Why are the namespaces Jackson serializes into XML structured incorrectly

Viewed 889

I am trying to serialize an object into XML using Jackson's XmlMapper. I want it to have namespaces in appropriate areas. When I serialize the object, however, every attribute is having a namespace - attributes that are directly part of the class I'm trying to serialize will have an xmlns="", and each attribute on referenced classes contains their own namespace prefix (rather than a single prefix for the whole referenced class).

I'm using Jackson 2.9.8

The main class I want to serialize:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
        "name",
        "objectDescription",
        "exampleFieldClass"
})
@XmlRootElement(name = "ExampleClass", namespace = "urn:ExampleClass.dto")
public class ExampleClass
{

    @XmlElementRef(name = "name", namespace = "urn:ExampleClass.dto", type = JAXBElement.class, required = false)
    protected JAXBElement<String> name;
    @XmlElementRef(name = "objectDescription", namespace = "urn:ExampleClass.dto", type = JAXBElement.class, required = false)
    protected JAXBElement<String> objectDescription;
    @XmlElementRef(name = "exampleFieldClass", namespace = "urn:ExampleFieldClass.dto", type = JAXBElement.class, required = false)
    protected JAXBElement<ExampleFieldClass> ExampleFieldClass;

    public JAXBElement<String> getName() {
        return name;
    }

    public void setName(JAXBElement<String> name) {
        this.name = name;
    }

    public JAXBElement<String> getObjectDescription() {
        return objectDescription;
    }

    public void setObjectDescription(JAXBElement<String> objectDescription) {
        this.objectDescription = objectDescription;
    }

    public JAXBElement<ExampleFieldClass> getExampleFieldClass() {
        return ExampleFieldClass;
    }

    public void setExampleFieldClass(JAXBElement<ExampleFieldClass> ExampleFieldClass) {
        this.ExampleFieldClass = ExampleFieldClass;
    }
}

A special type:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "ExampleFieldClass", namespace = "urn:ExampleFieldClass.dto", propOrder = {
        "id", "dtoType"
})
public class ExampleFieldClass {

    @XmlElement(name = "id", namespace = "urn:ExampleFieldClass.dto")
    protected String id;
    @XmlElement(name = "dtoType", namespace = "urn:ExampleFieldClass.dto")
    protected String dtoType;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getDtoType() {
        return dtoType;
    }

    public void setDtoType(String dtoType) {
        this.dtoType = dtoType;
    }
}

The XmlMapper config and a test:

public class ExampleTest {

    private final ExampleObjectFactory factory = new ExampleObjectFactory();

    @Test
    public void testSerialize() throws Exception {
        XmlMapper mapper = getXmlMapper();

        ExampleFieldClass exampleFieldClass = new ExampleFieldClass();
        exampleFieldClass.setDtoType("dtoType");
        exampleFieldClass.setId("id");

        ExampleClass exampleClass = new ExampleClass();
        exampleClass.setName(factory.createExampleClassName("name"));
        exampleClass.setObjectDescription(factory.createExampleClassObjectDescription("description"));
        exampleClass.setExampleFieldClass(factory.createExampleClassExampleFieldClass(exampleFieldClass));

        String serialized = mapper.writeValueAsString(exampleClass);
        System.out.println(serialized);
    }

    private XmlMapper getXmlMapper() {
        JacksonXmlModule module = new JacksonXmlModule();
        module.addSerializer(JAXBElement.class, new JsonSerializer<JAXBElement>() {
            @Override
            public void serialize(JAXBElement value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                if (value.isNil()) {
                    gen.writeNull();
                } else {
                    gen.writeObject(value.getValue());
                }
            }
        });

        XmlMapper objectMapper = new XmlMapper(module);
        objectMapper.registerModule(new JaxbAnnotationModule());

        objectMapper.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        return objectMapper;
    }

    @XmlRegistry
    public static class ExampleObjectFactory {

        @XmlElementDecl(namespace = "urn:ExampleClass.dto", name = "name", scope = ExampleClass.class)
        public JAXBElement<String> createExampleClassName(String value) {
            QName _ExampleClassName_QNAME = new QName("urn:ExampleClass.dto", "name");
            return new JAXBElement<>(_ExampleClassName_QNAME, String.class, ExampleClass.class, value);
        }

        @XmlElementDecl(namespace = "urn:ExampleClass.dto", name = "objectDescription", scope = ExampleClass.class)
        public JAXBElement<String> createExampleClassObjectDescription(String value) {
            QName _ExampleClassObjectDescription_QNAME = new QName("urn:ExampleClass.dto", "objectDescription");
            return new JAXBElement<>(_ExampleClassObjectDescription_QNAME, String.class, ExampleClass.class, value);
        }

        @XmlElementDecl(namespace = "urn:ExampleFieldClass.dto", name = "ExampleFieldClass", scope = ExampleFieldClass.class)
        public JAXBElement<ExampleFieldClass> createExampleClassExampleFieldClass(ExampleFieldClass value) {
            QName _ExampleClassExampleFieldClass_QNAME = new QName("urn:ExampleFieldClass.dto", "ExampleFieldClass");
            return new JAXBElement<>(_ExampleClassExampleFieldClass_QNAME, ExampleFieldClass.class, ExampleClass.class, value);
        }
    }
}

The actual XML output is this:

<?xml version='1.0' encoding='UTF-8'?>
<ExampleClass xmlns="urn:ExampleClass.dto">
    <name xmlns="">name</name>
    <objectDescription xmlns="">description</objectDescription>
    <exampleFieldClass xmlns="">
        <wstxns1:id xmlns:wstxns1="urn:ExampleFieldClass.dto">id</wstxns1:id>
        <wstxns2:dtoType xmlns:wstxns2="urn:ExampleFieldClass.dto">dtoType</wstxns2:dtoType>
    </exampleFieldClass>
</ExampleClass>

The namespaces shouldn't be defined on every attribute, nor should there be ones defined as an empty String.

Something to note: the empty namespaces xmlns="" will not be there if I change the attribute annotations to @XmlElement; however, it will no longer handle JaxbElement properly (empty vs null).

Does anyone know what I'm doing wrong for this?

2 Answers

If you change definition of ExampleClass:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
        "name",
        "objectDescription",
        "exampleFieldClass"
})
@XmlRootElement(name = "ExampleClass", namespace = "urn:ExampleClass.dto")
public class ExampleClass
{
    
    @XmlElement(name = "name", namespace = "urn:ExampleClass.dto", required = false)
    protected String name;
    
    @XmlElement(name = "objectDescription", namespace = "urn:ExampleClass.dto", required = false)
    protected String objectDescription;
    
    @XmlElement(name = "exampleFieldClass", namespace = "urn:ExampleFieldClass.dto", required = false)
    protected ExampleFieldClass ExampleFieldClass;

}

And change creation of ExampleClass instance:

    XmlMapper mapper = getXmlMapper();

    ExampleFieldClass exampleFieldClass = new ExampleFieldClass();
    exampleFieldClass.setDtoType("dtoType");
    exampleFieldClass.setId("id");

    ExampleClass exampleClass = new ExampleClass();
    exampleClass.setName("name");
    exampleClass.setObjectDescription("description");
    exampleClass.setExampleFieldClass(exampleFieldClass);

    String serialized = mapper.writeValueAsString(exampleClass);

Output will be:

<?xml version='1.0' encoding='UTF-8'?>
<ExampleClass xmlns="urn:ExampleClass.dto">
  <name>name</name>
  <objectDescription>description</objectDescription>
  <wstxns1:exampleFieldClass xmlns:wstxns1="urn:ExampleFieldClass.dto">
    <wstxns1:id>id</wstxns1:id>
    <wstxns1:dtoType>dtoType</wstxns1:dtoType>
  </wstxns1:exampleFieldClass>
</ExampleClass>

Ok, after digging into jaxb/jackson/spring/google... a lot... I found the XmlJaxbAnnotationIntrospector. This class will look at the JAXB annotations on whatever classes are being serialized to determine information such as simple name, namespace, propOrder, etc. The problem lies in the findNamespace and findRootName methods.

findNamespace(Annotated ann): there's 2 problems here. The first is, when it tries to determine the namespace of a Class, it will only look for the XmlRootElement annotation. It will not look for other annotations that may contain namespace (in my case, XmlType). The same issue applies for the XmlElementRef annotation - the current method will look for XmlElement and XmlAttribute to determine the namespace; it does not look for XmlElementRef.

findRootName(Annotated ann): similar to the findNamespace issue, this will only look at the XmlRootElement when trying to find the namespace. For our situation, again, that does not exist and we need it to look at the XmlType annotation.

The solution that I have, and please let me know if there's a reason it should not be doing this, was to extend from the XmlJaxbAnnotationIntrospector, creating my own implementation that overrides findNamespace and findRootName. Each method is essentially the same, except that it also adds the search for the XmlType annotation and XmlElementRef annotation. Once this is created, just add it to the object mapper using:

objectMapper.setAnnotationIntrospector(new CustomXmlJaxbAnnotationIntrospector());

Something to note, for anyone who might read this and have an additional issue that I ran into, propOrder doesn't seem to handle inheritance by default. Therefore, if you have a parent class that has a propOrder, those attributes will not be first in your serialized xml. To get around this, in that CustomXmlJaxbAnnotationInstrospector class, also override the findSerializationPropertyOrder(AnnotatedClass annotatedClass) - in here, you can loop through the super classes (up until Object.class), and prepend each set of props to a new list/String[] that will contain them all.

Related