Skip to content

Commit 0493e50

Browse files
authored
Support for registering reference types (when they can't be inferred) (#545)
1 parent e776fc1 commit 0493e50

File tree

7 files changed

+309
-27
lines changed

7 files changed

+309
-27
lines changed

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ Compile / resourceGenerators += Def.task {
103103
Seq(file)
104104
}.taskValue
105105

106+
Test / parallelExecution := false
107+
106108
ThisBuild / githubWorkflowJavaVersions := Seq("[email protected]", "[email protected]")
107109
ThisBuild / githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("test", "mimaReportBinaryIssues")))
108110
ThisBuild / githubWorkflowTargetTags ++= Seq("v*")

src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanIntrospector.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ import scala.reflect.NameTransformer
3737

3838
object BeanIntrospector {
3939

40-
private def getCtorParams(ctor: Constructor[_]): Seq[String] = {
41-
val names = JavaParameterIntrospector.getCtorParamNames(ctor)
42-
names.map(NameTransformer.decode)
43-
}
44-
4540
def apply[T <: AnyRef](cls: Class[_]) = {
4641

4742
/**
@@ -246,4 +241,9 @@ object BeanIntrospector {
246241

247242
BeanDescriptor(cls, fields ++ methods ++ lazyValMethods)
248243
}
244+
245+
private def getCtorParams(ctor: Constructor[_]): Seq[String] = {
246+
val names = JavaParameterIntrospector.getCtorParamNames(ctor)
247+
names.map(NameTransformer.decode)
248+
}
249249
}

src/main/scala/com/fasterxml/jackson/module/scala/introspect/ScalaAnnotationIntrospectorModule.scala

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,75 @@
11
package com.fasterxml.jackson.module.scala.introspect
22

3-
import java.lang.annotation.Annotation
43
import com.fasterxml.jackson.annotation.JsonCreator
5-
import com.fasterxml.jackson.databind.`type`.ClassKey
4+
import com.fasterxml.jackson.databind.`type`.{ClassKey, CollectionLikeType, MapLikeType, ReferenceType, SimpleType}
65
import com.fasterxml.jackson.databind.cfg.MapperConfig
76
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
87
import com.fasterxml.jackson.databind.deser._
98
import com.fasterxml.jackson.databind.introspect._
109
import com.fasterxml.jackson.databind.util.{AccessPattern, LRUMap, LookupCache}
11-
import com.fasterxml.jackson.databind.{BeanDescription, DeserializationConfig, DeserializationContext, DeserializationFeature, MapperFeature}
10+
import com.fasterxml.jackson.databind.{BeanDescription, DeserializationConfig, DeserializationContext, JavaType, MapperFeature}
1211
import com.fasterxml.jackson.module.scala.JacksonModule
1312
import com.fasterxml.jackson.module.scala.util.Implicits._
1413

14+
import java.lang.annotation.Annotation
15+
1516
object ScalaAnnotationIntrospector extends NopAnnotationIntrospector with ValueInstantiators {
16-
private [this] var _descriptorCache: LookupCache[ClassKey, BeanDescriptor] =
17+
private[this] var _descriptorCache: LookupCache[ClassKey, BeanDescriptor] =
1718
new LRUMap[ClassKey, BeanDescriptor](16, 100)
1819

20+
case class ClassHolder(valueClass: Option[Class[_]] = None)
21+
private case class ClassOverrides(overrides: scala.collection.mutable.Map[String, ClassHolder] = scala.collection.mutable.Map.empty)
22+
23+
private val overrideMap = scala.collection.mutable.Map[Class[_], ClassOverrides]()
24+
25+
/**
26+
* jackson-module-scala does not always properly handle deserialization of Options or Collections wrapping
27+
* Scala primitives (eg Int, Long, Boolean). There are general issues with serializing and deserializing
28+
* Scala 2 Enumerations. This function will not help with Enumerations.
29+
* <p>
30+
* This function is experimental and may be removed or significantly reworked in a later release.
31+
* <p>
32+
* These issues can be worked around by adding Jackson annotations on the affected fields.
33+
* This function is designed to be used when it is not possible to apply Jackson annotations.
34+
*
35+
* @param clazz the (case) class
36+
* @param fieldName the field name in the (case) class
37+
* @param referencedType the referenced type of the field - for `Option[Long]` - the referenced type is `Long`
38+
* @see [[clearRegisteredReferencedTypes()]]
39+
* @see [[clearRegisteredReferencedTypes(Class[_])]]
40+
* @since 2.13.0
41+
*/
42+
def registerReferencedValueType(clazz: Class[_], fieldName: String, referencedType: Class[_]): Unit = {
43+
val overrides = overrideMap.getOrElseUpdate(clazz, ClassOverrides()).overrides
44+
overrides.get(fieldName) match {
45+
case Some(holder) => overrides.put(fieldName, holder.copy(valueClass = Some(referencedType)))
46+
case _ => overrides.put(fieldName, ClassHolder(valueClass = Some(referencedType)))
47+
}
48+
}
49+
50+
/**
51+
* clears the state associated with reference types for the given class
52+
*
53+
* @param clazz the class for which to remove the registered reference types
54+
* @see [[registerReferencedValueType]]
55+
* @see [[clearRegisteredReferencedTypes()]]
56+
* @since 2.13.0
57+
*/
58+
def clearRegisteredReferencedTypes(clazz: Class[_]): Unit = {
59+
overrideMap.remove(clazz)
60+
}
61+
62+
/**
63+
* clears all the state associated with reference types
64+
*
65+
* @see [[registerReferencedValueType]]
66+
* @see [[clearRegisteredReferencedTypes(Class[_])]]
67+
* @since 2.13.0
68+
*/
69+
def clearRegisteredReferencedTypes(): Unit = {
70+
overrideMap.clear()
71+
}
72+
1973
def setDescriptorCache(cache: LookupCache[ClassKey, BeanDescriptor]): LookupCache[ClassKey, BeanDescriptor] = {
2074
val existingCache = _descriptorCache
2175
_descriptorCache = cache
@@ -106,22 +160,38 @@ object ScalaAnnotationIntrospector extends NopAnnotationIntrospector with ValueI
106160
extends StdValueInstantiator(delegate) {
107161

108162
private val overriddenConstructorArguments: Array[SettableBeanProperty] = {
163+
val overrides = overrideMap.get(descriptor.beanType).map(_.overrides.toMap).getOrElse(Map.empty)
109164
val applyDefaultValues = config.isEnabled(MapperFeature.APPLY_DEFAULT_VALUES)
110165
val args = delegate.getFromObjectArguments(config)
111166
Option(args) match {
112-
case Some(array) if applyDefaultValues => {
167+
case Some(array) if (applyDefaultValues || overrides.nonEmpty) => {
113168
array.map {
114-
case creator: CreatorProperty =>
169+
case creator: CreatorProperty => {
115170
// Locate the constructor param that matches it
116171
descriptor.properties.find(_.param.exists(_.index == creator.getCreatorIndex)) match {
117-
case Some(PropertyDescriptor(name, Some(ConstructorParameter(_, _, Some(defaultValue))), _, _, _, _, _)) =>
118-
creator.withNullProvider(new NullValueProvider {
119-
override def getNullValue(ctxt: DeserializationContext): AnyRef = defaultValue()
120-
override def getNullAccessPattern: AccessPattern = AccessPattern.DYNAMIC
121-
})
172+
case Some(pd) => {
173+
val mappedCreator = overrides.get(pd.name) match {
174+
case Some(refHolder) => WrappedCreatorProperty(creator, refHolder)
175+
case _ => creator
176+
}
177+
if (applyDefaultValues) {
178+
pd match {
179+
case PropertyDescriptor(_, Some(ConstructorParameter(_, _, Some(defaultValue))), _, _, _, _, _) => {
180+
mappedCreator.withNullProvider(new NullValueProvider {
181+
override def getNullValue(ctxt: DeserializationContext): AnyRef = defaultValue()
182+
183+
override def getNullAccessPattern: AccessPattern = AccessPattern.DYNAMIC
184+
})
185+
}
186+
case _ => mappedCreator
187+
}
188+
} else {
189+
mappedCreator
190+
}
191+
}
122192
case _ => creator
123193
}
124-
case other => other
194+
}
125195
}
126196
}
127197
case Some(array) => array
@@ -140,7 +210,7 @@ object ScalaAnnotationIntrospector extends NopAnnotationIntrospector with ValueI
140210
if (isMaybeScalaBeanType(beanDesc.getBeanClass)) {
141211

142212
_descriptorFor(beanDesc.getBeanClass).map { descriptor =>
143-
if (descriptor.properties.exists(_.param.exists(_.defaultValue.isDefined))) {
213+
if (overrideMap.contains(beanDesc.getBeanClass) || descriptor.properties.exists(_.param.exists(_.defaultValue.isDefined))) {
144214
defaultInstantiator match {
145215
case std: StdValueInstantiator =>
146216
new ScalaValueInstantiator(std, config, descriptor)
@@ -213,3 +283,21 @@ trait ScalaAnnotationIntrospectorModule extends JacksonModule {
213283
this += { _.appendAnnotationIntrospector(ScalaAnnotationIntrospector) }
214284
this += { _.addValueInstantiators(ScalaAnnotationIntrospector) }
215285
}
286+
287+
private case class WrappedCreatorProperty(creatorProperty: CreatorProperty, refHolder: ScalaAnnotationIntrospector.ClassHolder)
288+
extends CreatorProperty(creatorProperty, creatorProperty.getFullName) {
289+
290+
override def getType(): JavaType = {
291+
super.getType match {
292+
case rt: ReferenceType if refHolder.valueClass.isDefined =>
293+
ReferenceType.upgradeFrom(rt, SimpleType.constructUnsafe(refHolder.valueClass.get))
294+
case ct: CollectionLikeType if refHolder.valueClass.isDefined =>
295+
CollectionLikeType.upgradeFrom(ct, SimpleType.constructUnsafe(refHolder.valueClass.get))
296+
case mt: MapLikeType => {
297+
val valueType = refHolder.valueClass.map(SimpleType.constructUnsafe).getOrElse(mt.getContentType)
298+
MapLikeType.upgradeFrom(mt, mt.getKeyType, valueType)
299+
}
300+
case other => other
301+
}
302+
}
303+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.fasterxml.jackson.module.scala.deser
2+
3+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
4+
import com.fasterxml.jackson.module.scala.DefaultScalaModule
5+
import com.fasterxml.jackson.module.scala.introspect.ScalaAnnotationIntrospector
6+
import org.scalatest.BeforeAndAfterEach
7+
8+
object MapWithNumberValueDeserializerTest {
9+
case class AnnotatedMapLong(@JsonDeserialize(contentAs = classOf[java.lang.Long]) longs: Map[String, Long])
10+
case class AnnotatedMapPrimitiveLong(@JsonDeserialize(contentAs = classOf[Long]) longs: Map[String, Long])
11+
case class MapLong(longs: Map[String, Long])
12+
case class MapJavaLong(longs: Map[String, java.lang.Long])
13+
case class MapBigInt(longs: Map[String, BigInt])
14+
}
15+
16+
class MapWithNumberValueDeserializerTest extends DeserializerTest with BeforeAndAfterEach {
17+
lazy val module: DefaultScalaModule.type = DefaultScalaModule
18+
import MapWithNumberValueDeserializerTest._
19+
20+
private def sumMapLong(m: Map[String, Long]): Long = m.values.sum
21+
private def sumMapJavaLong(m: Map[String, java.lang.Long]): Long = m.values.map(_.toLong).sum
22+
private def sumMapBigInt(m: Map[String, BigInt]): Long = m.values.sum.toLong
23+
24+
override def afterEach(): Unit = {
25+
super.afterEach()
26+
ScalaAnnotationIntrospector.clearRegisteredReferencedTypes()
27+
}
28+
29+
"JacksonModuleScala" should "deserialize AnnotatedMapLong" in {
30+
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[AnnotatedMapLong])
31+
v1 shouldBe AnnotatedMapLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
32+
sumMapLong(v1.longs) shouldBe 456L
33+
}
34+
35+
it should "deserialize AnnotatedMapPrimitiveLong" in {
36+
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[AnnotatedMapPrimitiveLong])
37+
v1 shouldBe AnnotatedMapPrimitiveLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
38+
sumMapLong(v1.longs) shouldBe 456L
39+
}
40+
41+
it should "deserialize MapLong" in {
42+
ScalaAnnotationIntrospector.registerReferencedValueType(classOf[MapLong], "longs", classOf[Long])
43+
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[MapLong])
44+
v1 shouldBe MapLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
45+
//this will next call will fail with a Scala unboxing exception unless you ScalaAnnotationIntrospector.registerReferencedValueType
46+
//or use one of the equivalent classes in MapWithNumberDeserializerTest
47+
sumMapLong(v1.longs) shouldBe 456L
48+
}
49+
50+
it should "deserialize MapJavaLong" in {
51+
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[MapJavaLong])
52+
v1 shouldBe MapJavaLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
53+
sumMapJavaLong(v1.longs) shouldBe 456L
54+
}
55+
56+
it should "deserialize MapBigInt" in {
57+
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[MapBigInt])
58+
v1 shouldBe MapBigInt(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
59+
sumMapBigInt(v1.longs) shouldBe 456L
60+
}
61+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.fasterxml.jackson.module.scala.deser
2+
3+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
4+
import com.fasterxml.jackson.module.scala.DefaultScalaModule
5+
import com.fasterxml.jackson.module.scala.introspect.ScalaAnnotationIntrospector
6+
import org.scalatest.BeforeAndAfterEach
7+
8+
object OptionWithBooleanDeserializerTest {
9+
case class AnnotatedOptionBoolean(@JsonDeserialize(contentAs = classOf[java.lang.Boolean]) valueBoolean: Option[Boolean])
10+
case class AnnotatedOptionPrimitiveBoolean(@JsonDeserialize(contentAs = classOf[Boolean]) valueBoolean: Option[Boolean])
11+
case class OptionBoolean(valueBoolean: Option[Boolean])
12+
case class OptionJavaBoolean(valueBoolean: Option[java.lang.Boolean])
13+
}
14+
15+
class OptionWithBooleanDeserializerTest extends DeserializerTest with BeforeAndAfterEach {
16+
lazy val module: DefaultScalaModule.type = DefaultScalaModule
17+
import OptionWithBooleanDeserializerTest._
18+
19+
private def useOptionBoolean(v: Option[Boolean]): String = v.map(_.toString).getOrElse("null")
20+
private def useOptionJavaBoolean(v: Option[java.lang.Boolean]): String = v.map(_.toString).getOrElse("null")
21+
22+
override def afterEach(): Unit = {
23+
super.afterEach()
24+
ScalaAnnotationIntrospector.clearRegisteredReferencedTypes()
25+
}
26+
27+
"JacksonModuleScala" should "deserialize AnnotatedOptionBoolean" in {
28+
val v1 = deserialize("""{"valueBoolean":false}""", classOf[AnnotatedOptionBoolean])
29+
v1 shouldBe AnnotatedOptionBoolean(Some(false))
30+
v1.valueBoolean.get shouldBe false
31+
useOptionBoolean(v1.valueBoolean) shouldBe "false"
32+
}
33+
34+
it should "deserialize AnnotatedOptionPrimitiveBoolean" in {
35+
val v1 = deserialize("""{"valueBoolean":false}""", classOf[AnnotatedOptionPrimitiveBoolean])
36+
v1 shouldBe AnnotatedOptionPrimitiveBoolean(Some(false))
37+
v1.valueBoolean.get shouldBe false
38+
useOptionBoolean(v1.valueBoolean) shouldBe "false"
39+
}
40+
41+
it should "deserialize OptionBoolean (without registerReferencedValueType)" in {
42+
val v1 = deserialize("""{"valueBoolean":false}""", classOf[OptionBoolean])
43+
v1 shouldBe OptionBoolean(Some(false))
44+
v1.valueBoolean.get shouldBe false
45+
useOptionBoolean(v1.valueBoolean) shouldBe "false"
46+
}
47+
48+
it should "deserialize OptionBoolean (with registerReferencedValueType)" in {
49+
ScalaAnnotationIntrospector.registerReferencedValueType(classOf[OptionBoolean], "valueBoolean", classOf[Boolean])
50+
val v1 = deserialize("""{"valueBoolean":false}""", classOf[OptionBoolean])
51+
v1 shouldBe OptionBoolean(Some(false))
52+
v1.valueBoolean.get shouldBe false
53+
useOptionBoolean(v1.valueBoolean) shouldBe "false"
54+
}
55+
56+
it should "deserialize OptionJavaBoolean" in {
57+
val v1 = deserialize("""{"valueBoolean":false}""", classOf[OptionJavaBoolean])
58+
v1 shouldBe OptionJavaBoolean(Some(false))
59+
v1.valueBoolean.get shouldBe false
60+
useOptionJavaBoolean(v1.valueBoolean) shouldBe "false"
61+
}
62+
}

src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionWithNumberDeserializerTest.scala

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.fasterxml.jackson.module.scala.deser
22

33
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
44
import com.fasterxml.jackson.module.scala.DefaultScalaModule
5+
import com.fasterxml.jackson.module.scala.introspect.ScalaAnnotationIntrospector
6+
import org.scalatest.BeforeAndAfterEach
57

68
object OptionWithNumberDeserializerTest {
79
case class AnnotatedOptionLong(@JsonDeserialize(contentAs = classOf[java.lang.Long]) valueLong: Option[Long])
@@ -11,45 +13,51 @@ object OptionWithNumberDeserializerTest {
1113
case class OptionBigInt(value: Option[BigInt])
1214
}
1315

14-
class OptionWithNumberDeserializerTest extends DeserializerTest {
16+
class OptionWithNumberDeserializerTest extends DeserializerTest with BeforeAndAfterEach {
1517
lazy val module: DefaultScalaModule.type = DefaultScalaModule
1618
import OptionWithNumberDeserializerTest._
1719

1820
private def useOptionLong(v: Option[Long]): Long = v.map(_ * 2).getOrElse(0L)
1921
private def useOptionJavaLong(v: Option[java.lang.Long]): Long = v.map(_ * 2).getOrElse(0L)
2022
private def useOptionBigInt(v: Option[BigInt]): Long = v.map(_ * 2).map(_.toLong).getOrElse(0L)
2123

22-
"JacksonModuleScala" should "support AnnotatedOptionLong" in {
24+
override def afterEach(): Unit = {
25+
super.afterEach()
26+
ScalaAnnotationIntrospector.clearRegisteredReferencedTypes()
27+
}
28+
29+
"JacksonModuleScala" should "deserialize AnnotatedOptionLong" in {
2330
val v1 = deserialize("""{"valueLong":151}""", classOf[AnnotatedOptionLong])
2431
v1 shouldBe AnnotatedOptionLong(Some(151L))
2532
v1.valueLong.get shouldBe 151L
2633
useOptionLong(v1.valueLong) shouldBe 302L
2734
}
2835

29-
it should "support AnnotatedOptionPrimitiveLong" in {
36+
it should "deserialize AnnotatedOptionPrimitiveLong" in {
3037
val v1 = deserialize("""{"valueLong":151}""", classOf[AnnotatedOptionPrimitiveLong])
3138
v1 shouldBe AnnotatedOptionPrimitiveLong(Some(151L))
3239
v1.valueLong.get shouldBe 151L
3340
useOptionLong(v1.valueLong) shouldBe 302L
3441
}
3542

36-
it should "support OptionLong" in {
43+
it should "deserialize OptionLong" in {
44+
ScalaAnnotationIntrospector.registerReferencedValueType(classOf[OptionLong], "valueLong", classOf[Long])
3745
val v1 = deserialize("""{"valueLong":151}""", classOf[OptionLong])
3846
v1 shouldBe OptionLong(Some(151L))
3947
v1.valueLong.get shouldBe 151L
40-
//next assert fails due to unboxing issue -- without the @JsonDeserialize to help, jackson will
41-
//erroneously create an Option[Int] instead of Option[Long] leading to a class cast exception
42-
//useOptionLong(v1.valueLong) shouldBe 302L
48+
//this will next call will fail with a Scala unboxing exception unless you ScalaAnnotationIntrospector.registerReferencedValueType
49+
//or use one of the equivalent classes in OptionWithNumberDeserializerTest
50+
useOptionLong(v1.valueLong) shouldBe 302L
4351
}
4452

45-
it should "support OptionJavaLong" in {
53+
it should "deserialize OptionJavaLong" in {
4654
val v1 = deserialize("""{"valueLong":151}""", classOf[OptionJavaLong])
4755
v1 shouldBe OptionJavaLong(Some(151L))
4856
v1.valueLong.get shouldBe 151L
4957
useOptionJavaLong(v1.valueLong) shouldBe 302L
5058
}
5159

52-
it should "support OptionBigInt" in {
60+
it should "deserialize OptionBigInt" in {
5361
val v1 = deserialize("""{"value":151}""", classOf[OptionBigInt])
5462
v1 shouldBe OptionBigInt(Some(BigInt(151L)))
5563
v1.value.get shouldBe 151L

0 commit comments

Comments
 (0)