jackson-module-scala
jackson-module-scala copied to clipboard
Duplicate fields with ScalaObjectMapper
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?
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(_))
...
}
Can you provide an example of the case class that is causing problems?
@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}
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
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.
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.