jackson-module-scala icon indicating copy to clipboard operation
jackson-module-scala copied to clipboard

Duplicate fields with ScalaObjectMapper

Open reverofevil opened this issue 8 years ago • 6 comments

When there is a case class with BeanProperty-annotated field, the following code sometimes generates JSON with duplicate fields. For example, pId field gets serialized twice, as pId and pid.

private val mapper = {
  val objectMapper = new ObjectMapper with ScalaObjectMapper
  objectMapper.registerModule(DefaultScalaModule)
  objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
  objectMapper
}

def toJson[A <: AnyRef](a: A): String = {
  mapper.writeValueAsString(a)
}

It's impossible to remove BeanProperty annotation, because it's needed by another library. Also it's impossible to set BeanProperty on every field and remove ScalaObjectMapper, as such changes are numerous and would likely result in other compatibility issues. What's the proper way to fix this problem?

reverofevil avatar Jun 22 '16 16:06 reverofevil

Some strange code that may be related in ScalaPropertiesCollector

def findNameForSerialization(prop: PropertyDescriptor): Option[PropertyName] = {
  ...
  val paramName = annotatedParam.optMap(ai.findNameForDeserialization(_))
  val fieldName = annotatedField.optMap(ai.findNameForSerialization(_))
  ...
}

reverofevil avatar Jun 22 '16 17:06 reverofevil

Can you provide an example of the case class that is causing problems?

christophercurrie avatar Jul 19 '16 04:07 christophercurrie

@christophercurrie I better provide a complete example.

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import scala.beans.BeanProperty

case class Test(@BeanProperty pId: Long)

val mapper = {
  val objectMapper = new ObjectMapper with ScalaObjectMapper
  objectMapper.registerModule(DefaultScalaModule)
  objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
  objectMapper
}

println(mapper.writeValueAsString(Test(1L)))

Result is

{"pId":1,"pid":1}

reverofevil avatar Jul 19 '16 09:07 reverofevil

This appears to happen only when scala.beans.BeanProperty annotation is added - ScalaObjectMapper is not the cause (a plain ObjectMapper has same result) -- nor does setSerializationInclusion have any effect

pjfanning avatar Sep 03 '21 22:09 pjfanning

case class Test(@BeanProperty pId: Long)

@cowtowncoder the effect of the Scala @BeanProperty annotation (not to be confused with the jackson BeanProperty annotation) is that it adds a Java Bean style getPId() method to the class. Note the case, the initial p in the field name (pId) is uppercased while the other chars retain their case. legacyManglePropertyName in DefaultAccessorNamingStrategy in jackson-databind converts getPId() to a field name called 'pid' (all lowercase) which conflicts with the still accessible method pId (the original name of the scala field).

Is there a reason why legacyManglePropertyName in DefaultAccessorNamingStrategy lowercases the 'I' in getPId()?

Enabling MapperFeature.USE_STD_BEAN_NAMING has the opposite problem - getPId() evaluates to PId instead.

pjfanning avatar Sep 03 '21 22:09 pjfanning

So: name mangling is to do what Java Bean specification does, to derive property name from getter/setter. Originally ("legacy") I had misunderstood it so that the leading upper-case letters would always be lower-cased so that:

getURL() -> url (and getPId() -> pid)

but what Bean naming actually does is that lower-casing is only applied if there is one upper-case character; otherwise name is to be left as-is. Hence

getURL() -> URL (and getPId() -> PId)

However: field names are never mangled in any way. At least they should not.

The fundamental issue is deeper however: Scala (and Kotlin) start with property as a concept, matching field; then create accessors. But name mapping cannot be guaranteed as bijective (so that you can losslessly translate in either direction). Because of this, Jackson's attempts to unify logical property name fails.

It should not be impossible to reconcile these but... it has been tricky so far.

cowtowncoder avatar Sep 03 '21 23:09 cowtowncoder