Skip to content

Support for registering reference types (when they can't be inferred) #545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ Compile / resourceGenerators += Def.task {
Seq(file)
}.taskValue

Test / parallelExecution := false

ThisBuild / githubWorkflowJavaVersions := Seq("[email protected]", "[email protected]")
ThisBuild / githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("test", "mimaReportBinaryIssues")))
ThisBuild / githubWorkflowTargetTags ++= Seq("v*")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ import scala.reflect.NameTransformer

object BeanIntrospector {

private def getCtorParams(ctor: Constructor[_]): Seq[String] = {
val names = JavaParameterIntrospector.getCtorParamNames(ctor)
names.map(NameTransformer.decode)
}

def apply[T <: AnyRef](cls: Class[_]) = {

/**
Expand Down Expand Up @@ -246,4 +241,9 @@ object BeanIntrospector {

BeanDescriptor(cls, fields ++ methods ++ lazyValMethods)
}

private def getCtorParams(ctor: Constructor[_]): Seq[String] = {
val names = JavaParameterIntrospector.getCtorParamNames(ctor)
names.map(NameTransformer.decode)
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,75 @@
package com.fasterxml.jackson.module.scala.introspect

import java.lang.annotation.Annotation
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.databind.`type`.ClassKey
import com.fasterxml.jackson.databind.`type`.{ClassKey, CollectionLikeType, MapLikeType, ReferenceType, SimpleType}
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
import com.fasterxml.jackson.databind.deser._
import com.fasterxml.jackson.databind.introspect._
import com.fasterxml.jackson.databind.util.{AccessPattern, LRUMap, LookupCache}
import com.fasterxml.jackson.databind.{BeanDescription, DeserializationConfig, DeserializationContext, DeserializationFeature, MapperFeature}
import com.fasterxml.jackson.databind.{BeanDescription, DeserializationConfig, DeserializationContext, JavaType, MapperFeature}
import com.fasterxml.jackson.module.scala.JacksonModule
import com.fasterxml.jackson.module.scala.util.Implicits._

import java.lang.annotation.Annotation

object ScalaAnnotationIntrospector extends NopAnnotationIntrospector with ValueInstantiators {
private [this] var _descriptorCache: LookupCache[ClassKey, BeanDescriptor] =
private[this] var _descriptorCache: LookupCache[ClassKey, BeanDescriptor] =
new LRUMap[ClassKey, BeanDescriptor](16, 100)

case class ClassHolder(valueClass: Option[Class[_]] = None)
private case class ClassOverrides(overrides: scala.collection.mutable.Map[String, ClassHolder] = scala.collection.mutable.Map.empty)

private val overrideMap = scala.collection.mutable.Map[Class[_], ClassOverrides]()

/**
* jackson-module-scala does not always properly handle deserialization of Options or Collections wrapping
* Scala primitives (eg Int, Long, Boolean). There are general issues with serializing and deserializing
* Scala 2 Enumerations. This function will not help with Enumerations.
* <p>
* This function is experimental and may be removed or significantly reworked in a later release.
* <p>
* These issues can be worked around by adding Jackson annotations on the affected fields.
* This function is designed to be used when it is not possible to apply Jackson annotations.
*
* @param clazz the (case) class
* @param fieldName the field name in the (case) class
* @param referencedType the referenced type of the field - for `Option[Long]` - the referenced type is `Long`
* @see [[clearRegisteredReferencedTypes()]]
* @see [[clearRegisteredReferencedTypes(Class[_])]]
* @since 2.13.0
*/
def registerReferencedValueType(clazz: Class[_], fieldName: String, referencedType: Class[_]): Unit = {
val overrides = overrideMap.getOrElseUpdate(clazz, ClassOverrides()).overrides
overrides.get(fieldName) match {
case Some(holder) => overrides.put(fieldName, holder.copy(valueClass = Some(referencedType)))
case _ => overrides.put(fieldName, ClassHolder(valueClass = Some(referencedType)))
}
}

/**
* clears the state associated with reference types for the given class
*
* @param clazz the class for which to remove the registered reference types
* @see [[registerReferencedValueType]]
* @see [[clearRegisteredReferencedTypes()]]
* @since 2.13.0
*/
def clearRegisteredReferencedTypes(clazz: Class[_]): Unit = {
overrideMap.remove(clazz)
}

/**
* clears all the state associated with reference types
*
* @see [[registerReferencedValueType]]
* @see [[clearRegisteredReferencedTypes(Class[_])]]
* @since 2.13.0
*/
def clearRegisteredReferencedTypes(): Unit = {
overrideMap.clear()
}

def setDescriptorCache(cache: LookupCache[ClassKey, BeanDescriptor]): LookupCache[ClassKey, BeanDescriptor] = {
val existingCache = _descriptorCache
_descriptorCache = cache
Expand Down Expand Up @@ -106,22 +160,38 @@ object ScalaAnnotationIntrospector extends NopAnnotationIntrospector with ValueI
extends StdValueInstantiator(delegate) {

private val overriddenConstructorArguments: Array[SettableBeanProperty] = {
val overrides = overrideMap.get(descriptor.beanType).map(_.overrides.toMap).getOrElse(Map.empty)
val applyDefaultValues = config.isEnabled(MapperFeature.APPLY_DEFAULT_VALUES)
val args = delegate.getFromObjectArguments(config)
Option(args) match {
case Some(array) if applyDefaultValues => {
case Some(array) if (applyDefaultValues || overrides.nonEmpty) => {
array.map {
case creator: CreatorProperty =>
case creator: CreatorProperty => {
// Locate the constructor param that matches it
descriptor.properties.find(_.param.exists(_.index == creator.getCreatorIndex)) match {
case Some(PropertyDescriptor(name, Some(ConstructorParameter(_, _, Some(defaultValue))), _, _, _, _, _)) =>
creator.withNullProvider(new NullValueProvider {
override def getNullValue(ctxt: DeserializationContext): AnyRef = defaultValue()
override def getNullAccessPattern: AccessPattern = AccessPattern.DYNAMIC
})
case Some(pd) => {
val mappedCreator = overrides.get(pd.name) match {
case Some(refHolder) => WrappedCreatorProperty(creator, refHolder)
case _ => creator
}
if (applyDefaultValues) {
pd match {
case PropertyDescriptor(_, Some(ConstructorParameter(_, _, Some(defaultValue))), _, _, _, _, _) => {
mappedCreator.withNullProvider(new NullValueProvider {
override def getNullValue(ctxt: DeserializationContext): AnyRef = defaultValue()

override def getNullAccessPattern: AccessPattern = AccessPattern.DYNAMIC
})
}
case _ => mappedCreator
}
} else {
mappedCreator
}
}
case _ => creator
}
case other => other
}
}
}
case Some(array) => array
Expand All @@ -140,7 +210,7 @@ object ScalaAnnotationIntrospector extends NopAnnotationIntrospector with ValueI
if (isMaybeScalaBeanType(beanDesc.getBeanClass)) {

_descriptorFor(beanDesc.getBeanClass).map { descriptor =>
if (descriptor.properties.exists(_.param.exists(_.defaultValue.isDefined))) {
if (overrideMap.contains(beanDesc.getBeanClass) || descriptor.properties.exists(_.param.exists(_.defaultValue.isDefined))) {
defaultInstantiator match {
case std: StdValueInstantiator =>
new ScalaValueInstantiator(std, config, descriptor)
Expand Down Expand Up @@ -213,3 +283,21 @@ trait ScalaAnnotationIntrospectorModule extends JacksonModule {
this += { _.appendAnnotationIntrospector(ScalaAnnotationIntrospector) }
this += { _.addValueInstantiators(ScalaAnnotationIntrospector) }
}

private case class WrappedCreatorProperty(creatorProperty: CreatorProperty, refHolder: ScalaAnnotationIntrospector.ClassHolder)
extends CreatorProperty(creatorProperty, creatorProperty.getFullName) {

override def getType(): JavaType = {
super.getType match {
case rt: ReferenceType if refHolder.valueClass.isDefined =>
ReferenceType.upgradeFrom(rt, SimpleType.constructUnsafe(refHolder.valueClass.get))
case ct: CollectionLikeType if refHolder.valueClass.isDefined =>
CollectionLikeType.upgradeFrom(ct, SimpleType.constructUnsafe(refHolder.valueClass.get))
case mt: MapLikeType => {
val valueType = refHolder.valueClass.map(SimpleType.constructUnsafe).getOrElse(mt.getContentType)
MapLikeType.upgradeFrom(mt, mt.getKeyType, valueType)
}
case other => other
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.fasterxml.jackson.module.scala.deser

import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.introspect.ScalaAnnotationIntrospector
import org.scalatest.BeforeAndAfterEach

object MapWithNumberValueDeserializerTest {
case class AnnotatedMapLong(@JsonDeserialize(contentAs = classOf[java.lang.Long]) longs: Map[String, Long])
case class AnnotatedMapPrimitiveLong(@JsonDeserialize(contentAs = classOf[Long]) longs: Map[String, Long])
case class MapLong(longs: Map[String, Long])
case class MapJavaLong(longs: Map[String, java.lang.Long])
case class MapBigInt(longs: Map[String, BigInt])
}

class MapWithNumberValueDeserializerTest extends DeserializerTest with BeforeAndAfterEach {
lazy val module: DefaultScalaModule.type = DefaultScalaModule
import MapWithNumberValueDeserializerTest._

private def sumMapLong(m: Map[String, Long]): Long = m.values.sum
private def sumMapJavaLong(m: Map[String, java.lang.Long]): Long = m.values.map(_.toLong).sum
private def sumMapBigInt(m: Map[String, BigInt]): Long = m.values.sum.toLong

override def afterEach(): Unit = {
super.afterEach()
ScalaAnnotationIntrospector.clearRegisteredReferencedTypes()
}

"JacksonModuleScala" should "deserialize AnnotatedMapLong" in {
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[AnnotatedMapLong])
v1 shouldBe AnnotatedMapLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
sumMapLong(v1.longs) shouldBe 456L
}

it should "deserialize AnnotatedMapPrimitiveLong" in {
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[AnnotatedMapPrimitiveLong])
v1 shouldBe AnnotatedMapPrimitiveLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
sumMapLong(v1.longs) shouldBe 456L
}

it should "deserialize MapLong" in {
ScalaAnnotationIntrospector.registerReferencedValueType(classOf[MapLong], "longs", classOf[Long])
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[MapLong])
v1 shouldBe MapLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
//this will next call will fail with a Scala unboxing exception unless you ScalaAnnotationIntrospector.registerReferencedValueType
//or use one of the equivalent classes in MapWithNumberDeserializerTest
sumMapLong(v1.longs) shouldBe 456L
}

it should "deserialize MapJavaLong" in {
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[MapJavaLong])
v1 shouldBe MapJavaLong(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
sumMapJavaLong(v1.longs) shouldBe 456L
}

it should "deserialize MapBigInt" in {
val v1 = deserialize("""{"longs":{"151":151,"152":152,"153":153}}""", classOf[MapBigInt])
v1 shouldBe MapBigInt(Map("151" -> 151L, "152" -> 152L, "153" -> 153L))
sumMapBigInt(v1.longs) shouldBe 456L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.fasterxml.jackson.module.scala.deser

import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.introspect.ScalaAnnotationIntrospector
import org.scalatest.BeforeAndAfterEach

object OptionWithBooleanDeserializerTest {
case class AnnotatedOptionBoolean(@JsonDeserialize(contentAs = classOf[java.lang.Boolean]) valueBoolean: Option[Boolean])
case class AnnotatedOptionPrimitiveBoolean(@JsonDeserialize(contentAs = classOf[Boolean]) valueBoolean: Option[Boolean])
case class OptionBoolean(valueBoolean: Option[Boolean])
case class OptionJavaBoolean(valueBoolean: Option[java.lang.Boolean])
}

class OptionWithBooleanDeserializerTest extends DeserializerTest with BeforeAndAfterEach {
lazy val module: DefaultScalaModule.type = DefaultScalaModule
import OptionWithBooleanDeserializerTest._

private def useOptionBoolean(v: Option[Boolean]): String = v.map(_.toString).getOrElse("null")
private def useOptionJavaBoolean(v: Option[java.lang.Boolean]): String = v.map(_.toString).getOrElse("null")

override def afterEach(): Unit = {
super.afterEach()
ScalaAnnotationIntrospector.clearRegisteredReferencedTypes()
}

"JacksonModuleScala" should "deserialize AnnotatedOptionBoolean" in {
val v1 = deserialize("""{"valueBoolean":false}""", classOf[AnnotatedOptionBoolean])
v1 shouldBe AnnotatedOptionBoolean(Some(false))
v1.valueBoolean.get shouldBe false
useOptionBoolean(v1.valueBoolean) shouldBe "false"
}

it should "deserialize AnnotatedOptionPrimitiveBoolean" in {
val v1 = deserialize("""{"valueBoolean":false}""", classOf[AnnotatedOptionPrimitiveBoolean])
v1 shouldBe AnnotatedOptionPrimitiveBoolean(Some(false))
v1.valueBoolean.get shouldBe false
useOptionBoolean(v1.valueBoolean) shouldBe "false"
}

it should "deserialize OptionBoolean (without registerReferencedValueType)" in {
val v1 = deserialize("""{"valueBoolean":false}""", classOf[OptionBoolean])
v1 shouldBe OptionBoolean(Some(false))
v1.valueBoolean.get shouldBe false
useOptionBoolean(v1.valueBoolean) shouldBe "false"
}

it should "deserialize OptionBoolean (with registerReferencedValueType)" in {
ScalaAnnotationIntrospector.registerReferencedValueType(classOf[OptionBoolean], "valueBoolean", classOf[Boolean])
val v1 = deserialize("""{"valueBoolean":false}""", classOf[OptionBoolean])
v1 shouldBe OptionBoolean(Some(false))
v1.valueBoolean.get shouldBe false
useOptionBoolean(v1.valueBoolean) shouldBe "false"
}

it should "deserialize OptionJavaBoolean" in {
val v1 = deserialize("""{"valueBoolean":false}""", classOf[OptionJavaBoolean])
v1 shouldBe OptionJavaBoolean(Some(false))
v1.valueBoolean.get shouldBe false
useOptionJavaBoolean(v1.valueBoolean) shouldBe "false"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.fasterxml.jackson.module.scala.deser

import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.introspect.ScalaAnnotationIntrospector
import org.scalatest.BeforeAndAfterEach

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

class OptionWithNumberDeserializerTest extends DeserializerTest {
class OptionWithNumberDeserializerTest extends DeserializerTest with BeforeAndAfterEach {
lazy val module: DefaultScalaModule.type = DefaultScalaModule
import OptionWithNumberDeserializerTest._

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

"JacksonModuleScala" should "support AnnotatedOptionLong" in {
override def afterEach(): Unit = {
super.afterEach()
ScalaAnnotationIntrospector.clearRegisteredReferencedTypes()
}

"JacksonModuleScala" should "deserialize AnnotatedOptionLong" in {
val v1 = deserialize("""{"valueLong":151}""", classOf[AnnotatedOptionLong])
v1 shouldBe AnnotatedOptionLong(Some(151L))
v1.valueLong.get shouldBe 151L
useOptionLong(v1.valueLong) shouldBe 302L
}

it should "support AnnotatedOptionPrimitiveLong" in {
it should "deserialize AnnotatedOptionPrimitiveLong" in {
val v1 = deserialize("""{"valueLong":151}""", classOf[AnnotatedOptionPrimitiveLong])
v1 shouldBe AnnotatedOptionPrimitiveLong(Some(151L))
v1.valueLong.get shouldBe 151L
useOptionLong(v1.valueLong) shouldBe 302L
}

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

it should "support OptionJavaLong" in {
it should "deserialize OptionJavaLong" in {
val v1 = deserialize("""{"valueLong":151}""", classOf[OptionJavaLong])
v1 shouldBe OptionJavaLong(Some(151L))
v1.valueLong.get shouldBe 151L
useOptionJavaLong(v1.valueLong) shouldBe 302L
}

it should "support OptionBigInt" in {
it should "deserialize OptionBigInt" in {
val v1 = deserialize("""{"value":151}""", classOf[OptionBigInt])
v1 shouldBe OptionBigInt(Some(BigInt(151L)))
v1.value.get shouldBe 151L
Expand Down
Loading