jackson-module-scala
jackson-module-scala copied to clipboard
Inconsistent serializing/deserializing of Option[AnyVal] compared to AnyVal
scala> case class A(s: String) extends AnyVal defined class A
scala> case class B(a: A) defined class B
scala> objectMapper.writeValueAsString(B(A("foo"))) res1: String = {"a":"foo"}
scala> objectMapper.readValue(res1, classOf[B]) res2: B = B(A(foo))
scala> case class C(a: Option[A]) defined class C
--- Above all as expected ---
scala> objectMapper.writeValueAsString(C(Some(A("foo")))) res3: String = {"a":{"s":"foo"}}
scala> objectMapper.readValue(res3, classOf[C]) com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token at [Source: {"a":{"s":"foo"}}; line: 1, column: 2] at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:164) at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:749) at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:59) at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:12) at com.fasterxml.jackson.module.scala.deser.OptionDeserializer.deserialize(OptionDeserializerModule.scala:23) at com.fasterxml.jackson.module.scala.deser.OptionDeserializer.deserialize(OptionDeserializerModule.scala:13) at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:538) at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:348) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1058) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:268) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:124) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3051) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2146)
scala> objectMapper.readValue(res1, classOf[C]) res16: C = C(Some(foo))
Is this the expected behaviour? I found it very surprising.
Which version of the module are you using? I believe there's been a bugfix in this space recently, though that fix might not have yet made it out into a release.
Thanks for the reply. I was using 2.5.1 - I tested 2.5.2, 2.5.3-SNAPSHOT, and 2.6.0-SNAPSHOT and they all exhibit the same behaviour.
I'm encountering an issue with Option[AnyVal] as well. I've confirmed that it seems to be serializing and deserializing correctly with the fixes from https://github.com/FasterXML/jackson-module-scala/commit/f6cf609451d01f036e87ff1070abcb0b6e991b42, but the underlying deserialized type is incorrect.
Here is a full repro from a repl on the latest release:
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import scala.annotation.meta.getter
case class FooAnyVal(@(JsonValue @getter) underlying: String) extends AnyVal
case class FooAnyValHolder(value: Option[FooAnyVal])
val mapper = new ObjectMapper { registerModule(DefaultScalaModule) }
val value = FooAnyValHolder(Some(FooAnyVal("bar")))
val serialized = mapper.writeValueAsString(value)
val deserialized = mapper.readValue(serialized, classOf[FooAnyValHolder])
deserialized.value.foreach { v => println(v) }
throws
java.lang.ClassCastException: java.lang.String cannot be cast to FooAnyVal
at $anonfun$1.apply(<console>:32)
at scala.Option.foreach(Option.scala:257)
... 65 elided
Eh-eh. Very annoying problem...
+1
Is this fixed for scala 2.10? This is a big nuisance
Are there any workarounds to this?
@lure I'm not sure what you mean? So you can't use an anyval in an option?
+1, very annoying. We like to use named Strings, by extending AnyVal, so UserId is a type, not just a free form string. However, with this wrinkle, any place we use Option[UserId] will properly deserialize, but not serialize.
I don't work on Scala module so I don't know now or why, but it would be useful if this could be verified against latest released version of scala module, 2.8.7.
This because 2.8 has many fixes to handling of "ReferenceType"s; general JavaType abstraction that Jackson uses for Java 8 Optional, Guava option and JDK AtomicReference, as well as Scala's Option. It is possible that scala module hasn't been fully updated to benefit from rework.
Also while it seems unlikely that scala version matters here, perhaps verifying that it also occurs with Scala 2.12 (and scala module 2.8.7) would make sense.
@cowtowncoder using jackson-module-scala 2.8.7 and scala 2.11.8, I still get the casting exception for a wrapped string class that extends AnyVal.
Example of the AnyVal impl:
@JsonSerialize(using = classOf[TypedStringJacksonSerializer])
trait TypedString extends Any {
/** Wrapped string */
def asString: String
}
/** custom serializer */
private[ids] class TypedStringJacksonSerializer
extends StdSerializer[TypedString](classOf[TypedString]) {
override def serialize(
value: TypedString,
gen: JsonGenerator,
provider: SerializerProvider): Unit = gen.writeString(value.asString)
}
@twistedpair As I said I know very little about this module (or Scala for that matter); but thank you for providing the snippet. It should help whomever can help.