This is a workaround for XML collection duplicated element names that I was fixing recently.
We use case classes in Scala and the Jackson library for XML serialization. We have a class that has a list of some nested class. When serializing it, we would like to name the nested element properly.
We start with these classes:
1 2 3 4 5 6 7 |
case class Person(val Name: String) @JsonPropertyOrder(Array("People")) case class Base ( val People: List[Person] ) |
When we try to serialize it, we’ll get something like this:
1 2 3 4 5 6 7 |
< Base> < People> < People> < Name>Some name here</Name> </People> </People> </Base> |
You can see that we have Base -> People -> People
instead of Base -> People -> Person
. We can try to fix it with regular annotations:
1 2 3 4 5 6 |
case class Base ( @JacksonXmlElementWrapper(localName = "People") @JacksonXmlProperty(localName = "Person") val People: List[Person] ) |
It now serializes correctly. However, when you try to deserialize it, you get the following exception:
1 |
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Could not find creator property with name 'Person' |
This fails because we use JacksonXmlProperty
to rename the property and this gets handled incorrectly.
The issue is with how Scala implements case classes. Fields are always stored as private fields with getters (and setters if you use var
), and the annotations are propagated to the constructor parameters.
My way of fixing it was the following:
- Do not propagate annotations to the constructor
- Create another constructor that has a different signature that the default one
- Mark the constructor as
@JsonCreator
This way, Jackson creates the object with default values using the new constructor, and then sets the fields with reflection. So, this is how the code should look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
case class Base ( @(JacksonXmlElementWrapper @field @getter)(localName = "People") @(JacksonXmlProperty @field @getter)(localName = "Person") val People: List[Person] ) { @JsonCreator def this( v1: List[Person] ignored: String ) = { this(v1) } } |
You could go with some different constructor if needed (like a parameterless one and pass nulls explicitly).
This works with Jackson 2.13.0.