Search code examples
javaxmljacksonxml-serializationjackson-dataformat-xml

Dynamic root element with Jackson


I'm currently working on a project that deals with elements that (for legacy reasons) must have a tag name that represents their type.

Basically I have this:

@JsonRootName("node")
class NodeDocument {
    private String type;
}

Which outputs something like:

<node type="someType"></node>

But what's expected would be:

<someType></someType>

@JsonRootName doesn't seem to be usable on a method or attribute.

Even though there is SerializationConfig.withRooName() or custom serializers, I can't seem to find a way to define the root name with a dynamic value stored in the object itself.


Solution

  • I assume NodeDocument contains more than just one property. In that case you need to implement custom serialiser together with BeanSerializerModifier which allow you to serialise all properties. Below code shows complete solution:

    import com.fasterxml.jackson.annotation.JsonIgnore;
    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.BeanDescription;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializationConfig;
    import com.fasterxml.jackson.databind.SerializationFeature;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import com.fasterxml.jackson.databind.module.SimpleModule;
    import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
    import com.fasterxml.jackson.databind.util.NameTransformer;
    import com.fasterxml.jackson.dataformat.xml.XmlMapper;
    import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
    
    import javax.xml.namespace.QName;
    import java.io.IOException;
    import java.util.Objects;
    
    public class XmlJacksonApp {
    
        public static void main(String... args) throws Exception {
            SimpleModule dynamicRootNameModule = new SimpleModule();
            dynamicRootNameModule.setSerializerModifier(new DynamicRootNameBeanSerializerModifier());
    
            XmlMapper mapper = XmlMapper.xmlBuilder()
                    .enable(SerializationFeature.INDENT_OUTPUT)
                    .addModule(dynamicRootNameModule)
                    .build();
            NodeDocument element = new NodeDocument();
            element.setId(123);
            element.setName("Rick and Morty.doc");
            element.setType("sitcom");
    
            mapper.writeValue(System.out, element);
        }
    }
    
    class DynamicRootNameBeanSerializerModifier extends BeanSerializerModifier {
        @Override
        public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
            if (beanDesc.getBeanClass() == NodeDocument.class) {
                return new NodeDocumentJsonSerializer((JsonSerializer<NodeDocument>) serializer);
            }
            return super.modifySerializer(config, beanDesc, serializer);
        }
    }
    
    class NodeDocumentJsonSerializer extends JsonSerializer<NodeDocument> {
        private final JsonSerializer<NodeDocument> serializer;
    
        NodeDocumentJsonSerializer(JsonSerializer<NodeDocument> serializer) {
            this.serializer = Objects.requireNonNull(serializer);
        }
    
        @Override
        public void serialize(NodeDocument value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            ToXmlGenerator xmlGen = (ToXmlGenerator) gen;
            writeDynamicRootName(value.getType(), xmlGen);
            serializeProperties(value, gen, serializers);
            writeEndObject(xmlGen);
        }
    
        private void writeDynamicRootName(String rootName, ToXmlGenerator xmlGen) throws IOException {
            xmlGen.setNextName(new QName("", rootName));
            xmlGen.writeStartObject();
        }
    
        private void serializeProperties(NodeDocument value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            serializer.unwrappingSerializer(NameTransformer.NOP).serialize(value, gen, serializers);
        }
    
        private void writeEndObject(ToXmlGenerator xmlGen) throws IOException {
            xmlGen.writeEndObject();
        }
    }
    
    class NodeDocument {
    
        @JsonIgnore
        private String type;
        private int id;
        private String name;
    
        // getters, setters
    }
    

    Above code prints:

    <sitcom>
      <id>123</id>
      <name>Rick and Morty.doc</name>
    </sitcom>