FasterXML / jackson-module-scala

Add-on module for Jackson (https://github.com/FasterXML/jackson) to support Scala-specific datatypes
Apache License 2.0
501 stars 141 forks source link

XML collection duplicated element names #242

Open peregin55 opened 8 years ago

peregin55 commented 8 years ago

I'm having trouble getting a case class with a collection field to serialize the way I want. I have a data model that looks like this:

    @JsonRootName("document")
    case class Document(header: Header)
    case class Header(authors: Seq[Author])
    case class Author(name: String)

And I'm using Jackson to produce an XML:

    val doc = Document(Header(Seq(Author("Dick Jones"), Author("Bob Morton"))))
    val mapper = new XmlMapper()
    mapper.registerModule(DefaultScalaModule)
    val xml = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(doc)

The document that looks like this:

    <document>
      <header>
        <authors>
          <authors>
            <name>Dick Jones</name>
          </authors>
          <authors>
            <name>Bob Morton</name>
          </authors>
        </authors>
      </header>
    </document>

But <authors> is duplicated with its nested elements. I want the <authors> tag to contain a list of <author> elements. I can get this output by adding the following annotations to the Header case class:

    case class Header(@JacksonXmlElementWrapper(localName = "authors") @JacksonXmlProperty(localName = "author") authors: Seq[Author])

This serializes the way I want, but when deserializing I get the following exception:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Could not find creator property with name 'authors' (in class sample.Header)
at [Source: java.io.StringReader@3d778e7; line: 1, column: 1]
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:216)
at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:894)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:541)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:228)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:143)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:406)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:352)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
at com.fasterxml.jackson.databind.DeserializationContext.findContextualValueDeserializer(DeserializationContext.java:444)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.findDeserializer(StdDeserializer.java:946)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:446)
at com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer.resolve(DelegatingDeserializer.java:61)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:296)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:477)
at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:3890)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3785)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2779)
at sample.Main$.main(Message.scala:26)
at sample.Main.main(Message.scala)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Is there something I can do to get this to deserialize properly?
Thanks for your help!

cowtowncoder commented 8 years ago

I think this would belong under jackson-dataformat-xml, unless there is something Scala specific.

peregin55 commented 8 years ago

In Java this code works if I use the JavaBean pattern to create the domain model. It sounds like JacksonXmlElementWrapper doesn't really work very well with immutable Java objects [1].

In Scala I'd like to avoid JavaBean since case-classes use immutability. Is there a way to control the element names for Seq/immutable lists? Either with JacksonXmlElementWrapper or something else?

Thanks!

[1] https://github.com/FasterXML/jackson-dataformat-xml/issues/149

cowtowncoder commented 8 years ago

@peregin55 Perhaps this has more to do with problems with case classes, related to handling of @JsonCreator and constructors -- this is what exception also suggests. So I don't think it is XML-specific after all, but more likely Scala issue. So scratch the suggestion to move this.

However what would be useful would be equivalent json-only example, I think.

Vistritium commented 7 years ago

+1 have this issue with scala List

cowtowncoder commented 7 years ago

Come to think of it, there's a good chance this would be due to XML-specific extra handling that is required. But if anyone can double-check to ensure this can not be reproduced with json, that would be great. Building a combined test is problematic from module perspective; neither module should (ideally) depend on each either wrt releases.

ConradFinkelstein commented 6 years ago

+1 have this issue with scala List

afish commented 4 months ago

I had the same issue. I fixed it with adding new constructor with a different signature than the built-in one, and marking it as @JsonCreator. I also had to change annotations so they don't get applied on the constructor parameters.

This is how the code should look like (I didn't verify it with the case from this issue, so there may be typos):

case class Header(
    @(JacksonXmlElementWrapper @field @getter)(localName = "authors")
    @(JacksonXmlElementWrapper @field @getter)(localName = "author")
    authors: Seq[Author]) {
   @JsonCreator
   def this() = {
    this(null)
   }
}

See also this one: https://blog.adamfurmanek.pl/2024/05/20/serializing-collections-with-jackson-in-scala-and-renaming-the-nested-element/