Skip to content

Commit ef507be

Browse files
committed
Support for registering reference types (when they can't be inferred) (#545)
1 parent be03b52 commit ef507be

File tree

7 files changed

+327
-48
lines changed

7 files changed

+327
-48
lines changed

build.sbt

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

94+
Test / parallelExecution := false
95+
9496
ThisBuild / githubWorkflowJavaVersions := Seq("adopt@1.8", "adopt@1.11", "adopt@1.16")
9597
ThisBuild / githubWorkflowTargetTags ++= Seq("v*")
9698
ThisBuild / githubWorkflowPublishTargetBranches := Seq(

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: 119 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,76 @@ package com.fasterxml.jackson.module.scala.introspect
22

33
import com.fasterxml.jackson.annotation.JsonCreator
44
import com.fasterxml.jackson.databind.JacksonModule.SetupContext
5-
import com.fasterxml.jackson.databind.`type`.ClassKey
5+
import com.fasterxml.jackson.databind.`type`.{ClassKey, CollectionLikeType, MapLikeType, ReferenceType, SimpleType}
66
import com.fasterxml.jackson.databind.cfg.MapperConfig
77
import com.fasterxml.jackson.databind.deser._
88
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
99
import com.fasterxml.jackson.databind.introspect._
1010
import com.fasterxml.jackson.databind.util.{AccessPattern, LookupCache, SimpleLookupCache}
11-
import com.fasterxml.jackson.databind.{BeanDescription, DeserializationConfig, DeserializationContext, MapperFeature, PropertyName}
11+
import com.fasterxml.jackson.databind.{BeanDescription, DeserializationConfig, DeserializationContext, JavaType, MapperFeature, PropertyName}
1212
import com.fasterxml.jackson.module.scala.JacksonModule.InitializerBuilder
1313
import com.fasterxml.jackson.module.scala.{JacksonModule, ScalaModule}
1414
import com.fasterxml.jackson.module.scala.util.Implicits._
1515

1616
import java.lang.annotation.Annotation
1717

1818
class ScalaAnnotationIntrospectorInstance(config: ScalaModule.Config) extends NopAnnotationIntrospector with ValueInstantiators {
19-
private [this] var _descriptorCache: LookupCache[ClassKey, BeanDescriptor] =
19+
private[this] var _descriptorCache: LookupCache[ClassKey, BeanDescriptor] =
2020
new SimpleLookupCache[ClassKey, BeanDescriptor](16, 100)
2121

22+
case class ClassHolder(valueClass: Option[Class[_]] = None)
23+
private case class ClassOverrides(overrides: scala.collection.mutable.Map[String, ClassHolder] = scala.collection.mutable.Map.empty)
24+
25+
private val overrideMap = scala.collection.mutable.Map[Class[_], ClassOverrides]()
26+
27+
/**
28+
* jackson-module-scala does not always properly handle deserialization of Options or Collections wrapping
29+
* Scala primitives (eg Int, Long, Boolean). There are general issues with serializing and deserializing
30+
* Scala 2 Enumerations. This function will not help with Enumerations.
31+
* <p>
32+
* This function is experimental and may be removed or significantly reworked in a later release.
33+
* <p>
34+
* These issues can be worked around by adding Jackson annotations on the affected fields.
35+
* This function is designed to be used when it is not possible to apply Jackson annotations.
36+
*
37+
* @param clazz the (case) class
38+
* @param fieldName the field name in the (case) class
39+
* @param referencedType the referenced type of the field - for `Option[Long]` - the referenced type is `Long`
40+
* @see [[clearRegisteredReferencedTypes()]]
41+
* @see [[clearRegisteredReferencedTypes(Class[_])]]
42+
* @since 2.13.0
43+
*/
44+
def registerReferencedValueType(clazz: Class[_], fieldName: String, referencedType: Class[_]): Unit = {
45+
val overrides = overrideMap.getOrElseUpdate(clazz, ClassOverrides()).overrides
46+
overrides.get(fieldName) match {
47+
case Some(holder) => overrides.put(fieldName, holder.copy(valueClass = Some(referencedType)))
48+
case _ => overrides.put(fieldName, ClassHolder(valueClass = Some(referencedType)))
49+
}
50+
}
51+
52+
/**
53+
* clears the state associated with reference types for the given class
54+
*
55+
* @param clazz the class for which to remove the registered reference types
56+
* @see [[registerReferencedValueType]]
57+
* @see [[clearRegisteredReferencedTypes()]]
58+
* @since 2.13.0
59+
*/
60+
def clearRegisteredReferencedTypes(clazz: Class[_]): Unit = {
61+
overrideMap.remove(clazz)
62+
}
63+
64+
/**
65+
* clears all the state associated with reference types
66+
*
67+
* @see [[registerReferencedValueType]]
68+
* @see [[clearRegisteredReferencedTypes(Class[_])]]
69+
* @since 2.13.0
70+
*/
71+
def clearRegisteredReferencedTypes(): Unit = {
72+
overrideMap.clear()
73+
}
74+
2275
def setDescriptorCache(cache: LookupCache[ClassKey, BeanDescriptor]): LookupCache[ClassKey, BeanDescriptor] = {
2376
val existingCache = _descriptorCache
2477
_descriptorCache = cache
@@ -124,7 +177,7 @@ class ScalaAnnotationIntrospectorInstance(config: ScalaModule.Config) extends No
124177
defaultInstantiator: ValueInstantiator): ValueInstantiator = {
125178
if (isMaybeScalaBeanType(beanDesc.getBeanClass)) {
126179
_descriptorFor(beanDesc.getBeanClass).map { descriptor =>
127-
if (descriptor.properties.exists(_.param.exists(_.defaultValue.isDefined))) {
180+
if (overrideMap.contains(beanDesc.getBeanClass) || descriptor.properties.exists(_.param.exists(_.defaultValue.isDefined))) {
128181
defaultInstantiator match {
129182
case std: StdValueInstantiator =>
130183
new ScalaValueInstantiator(config, std, deserializationConfig, descriptor)
@@ -194,6 +247,55 @@ class ScalaAnnotationIntrospectorInstance(config: ScalaModule.Config) extends No
194247
case am: AnnotatedMember => isMaybeScalaBeanType(am.getDeclaringClass)
195248
}
196249
}
250+
251+
private class ScalaValueInstantiator(config: ScalaModule.Config, delegate: StdValueInstantiator,
252+
deserializationConfig: DeserializationConfig, descriptor: BeanDescriptor)
253+
extends StdValueInstantiator(delegate) {
254+
255+
private val overriddenConstructorArguments: Array[SettableBeanProperty] = {
256+
val overrides = overrideMap.get(descriptor.beanType).map(_.overrides.toMap).getOrElse(Map.empty)
257+
val applyDefaultValues = deserializationConfig.isEnabled(MapperFeature.APPLY_DEFAULT_VALUES)
258+
val args = delegate.getFromObjectArguments(deserializationConfig)
259+
Option(args) match {
260+
case Some(array) if (applyDefaultValues || overrides.nonEmpty) => {
261+
array.map {
262+
case creator: CreatorProperty => {
263+
// Locate the constructor param that matches it
264+
descriptor.properties.find(_.param.exists(_.index == creator.getCreatorIndex)) match {
265+
case Some(pd) => {
266+
val mappedCreator = overrides.get(pd.name) match {
267+
case Some(refHolder) => WrappedCreatorProperty(creator, refHolder)
268+
case _ => creator
269+
}
270+
if (applyDefaultValues) {
271+
pd match {
272+
case PropertyDescriptor(_, Some(ConstructorParameter(_, _, Some(defaultValue))), _, _, _, _, _) => {
273+
mappedCreator.withNullProvider(new NullValueProvider {
274+
override def getNullValue(ctxt: DeserializationContext): AnyRef = defaultValue()
275+
276+
override def getNullAccessPattern: AccessPattern = AccessPattern.DYNAMIC
277+
})
278+
}
279+
case _ => mappedCreator
280+
}
281+
} else {
282+
mappedCreator
283+
}
284+
}
285+
case _ => creator
286+
}
287+
}
288+
}
289+
}
290+
case Some(array) => array
291+
case _ => Array.empty
292+
}
293+
}
294+
295+
override def getFromObjectArguments(config: DeserializationConfig): Array[SettableBeanProperty] = {
296+
overriddenConstructorArguments
297+
}
298+
}
197299
}
198300

199301
trait ScalaAnnotationIntrospectorModule extends JacksonModule {
@@ -213,37 +315,20 @@ object ScalaAnnotationIntrospectorModule extends ScalaAnnotationIntrospectorModu
213315

214316
object ScalaAnnotationIntrospector extends ScalaAnnotationIntrospectorInstance(ScalaModule.defaultBuilder)
215317

216-
private class ScalaValueInstantiator(config: ScalaModule.Config, delegate: StdValueInstantiator,
217-
deserializationConfig: DeserializationConfig, descriptor: BeanDescriptor)
218-
extends StdValueInstantiator(delegate) {
219-
220-
private val overriddenConstructorArguments: Array[SettableBeanProperty] = {
221-
val applyDefaultValues = deserializationConfig.isEnabled(MapperFeature.APPLY_DEFAULT_VALUES) &&
222-
config.shouldApplyDefaultValuesWhenDeserializing()
223-
val args = delegate.getFromObjectArguments(deserializationConfig)
224-
Option(args) match {
225-
case Some(array) if applyDefaultValues => {
226-
array.map {
227-
case creator: CreatorProperty =>
228-
// Locate the constructor param that matches it
229-
descriptor.properties.find(_.param.exists(_.index == creator.getCreatorIndex)) match {
230-
case Some(PropertyDescriptor(name, Some(ConstructorParameter(_, _, Some(defaultValue))), _, _, _, _, _)) =>
231-
creator.withNullProvider(new NullValueProvider {
232-
override def getNullValue(ctxt: DeserializationContext): AnyRef = defaultValue()
233-
234-
override def getNullAccessPattern: AccessPattern = AccessPattern.DYNAMIC
235-
})
236-
case _ => creator
237-
}
238-
case other => other
239-
}
318+
private case class WrappedCreatorProperty(creatorProperty: CreatorProperty, refHolder: ScalaAnnotationIntrospector.ClassHolder)
319+
extends CreatorProperty(creatorProperty, creatorProperty.getFullName) {
320+
321+
override def getType(): JavaType = {
322+
super.getType match {
323+
case rt: ReferenceType if refHolder.valueClass.isDefined =>
324+
ReferenceType.upgradeFrom(rt, SimpleType.constructUnsafe(refHolder.valueClass.get))
325+
case ct: CollectionLikeType if refHolder.valueClass.isDefined =>
326+
CollectionLikeType.upgradeFrom(ct, SimpleType.constructUnsafe(refHolder.valueClass.get))
327+
case mt: MapLikeType => {
328+
val valueType = refHolder.valueClass.map(SimpleType.constructUnsafe).getOrElse(mt.getContentType)
329+
MapLikeType.upgradeFrom(mt, mt.getKeyType, valueType)
240330
}
241-
case Some(array) => array
242-
case _ => Array.empty
331+
case other => other
243332
}
244333
}
245-
246-
override def getFromObjectArguments(deserializationConfig: DeserializationConfig): Array[SettableBeanProperty] = {
247-
overriddenConstructorArguments
248-
}
249334
}
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+
}

0 commit comments

Comments
 (0)