Search code examples
javajacksonjackson-dataformat-xml

Jackson serialize list with polymorphic elements to XML


I have a Zoo type with a List<Animal> field, whose elements are objects of Animal's sub-classes. When I use Jackson to serialize a Zoo object to XML, I want to generate different tag names for elements of different types.

For example:

abstract class Animal {}

@JacksonXmlRootElement(localName = "Dog")
class Dog extends Animal {}

@JacksonXmlRootElement(localName = "Cat")
class Cat extends Animal {}

@JacksonXmlRootElement(localName = "Zoo")
public class Zoo {

  @JacksonXmlProperty
  @JacksonXmlElementWrapper(useWrapping = false)
  List<Animal> animals = new ArrayList<>();
}

Serialize a Zoo instance.

public static void main(String[] args) throws JsonProcessingException {
  Zoo zoo = new Zoo();
  zoo.animals.add(new Dog());
  zoo.animals.add(new Cat());
  zoo.animals.add(new Dog());
  String xml = new XmlMapper().writerWithDefaultPrettyPrinter()
      .writeValueAsString(zoo);
  System.out.println(xml);
}

I expect to get:

<Zoo>
  <Dog/>
  <Cat/>
  <Dog/>
</Zoo>

The actual output:

<Zoo>
  <animals/>
  <animals/>
  <animals/>
</Zoo>

Solution

  • It is possible to obtain the expected result with a custom serializer, but you loose all the advantages available with the use of JsonTypeInfo and you have to write a custom deserializer too. Anyway, you can define three simple classes like below:

    public abstract class Animal {}
    public class Dog extends Animal {}
    
    @JsonSerialize(using = ZooSerializer.class)
    public class Zoo {
      List<Animal> animals = new ArrayList<>();
    }
    

    Then you have to write your custom ZooSerializer serializer that will build your custom xml:

    public class ZooSerializer extends StdSerializer<Zoo> {
    
        public ZooSerializer(Class<Zoo> t) {
            super(t);
        }
        
        public ZooSerializer() {
            this(null);
        }
    
        @Override
        public void serialize(Zoo zoo, JsonGenerator jg, SerializerProvider sp) throws IOException {
            jg.writeStartObject();
            for (Animal animal: zoo.animals) {
                jg.writeNullField(animal.getClass().getSimpleName());
            }
            jg.writeEndObject();
        }
    }
    

    This will product the expected result at the price of loosing all the advantages due to the automatic serialization and deserialization that could be obtained with JsonTypeInfo and JsonSubTypes annotations over the two classes:

    Zoo zoo = new Zoo();
    zoo.animals.add(new Dog());
    zoo.animals.add(new Dog());
    /* the str content obtained by the serialization of zoo
    Zoo>
      <Dog/>
      <Dog/>
    </Zoo>
    */
    String str = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(zoo);