From ed96d1104c310242b4f95c68ec500c5d363ecec7 Mon Sep 17 00:00:00 2001 From: James Roper Date: Thu, 5 Dec 2019 14:45:08 +1100 Subject: [PATCH] Added default value support --- .../scala/introspect/BeanDescriptor.scala | 8 ++- .../scala/introspect/BeanIntrospector.scala | 19 ++++++- .../scala/introspect/PropertyDescriptor.scala | 2 +- .../ScalaAnnotationIntrospectorModule.scala | 52 ++++++++++++++++++- .../module/scala/deser/CreatorTest.scala | 13 +++++ 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanDescriptor.scala b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanDescriptor.scala index f15b4a406..0a378c90e 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanDescriptor.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanDescriptor.scala @@ -2,4 +2,10 @@ package com.fasterxml.jackson.module.scala.introspect import scala.language.existentials -case class BeanDescriptor(beanType: Class[_], properties: Seq[PropertyDescriptor]) \ No newline at end of file +case class BeanDescriptor(beanType: Class[_], properties: Seq[PropertyDescriptor]) { + val constructorParameters: Map[Int, ConstructorParameter] = { + properties.collect { + case PropertyDescriptor(_, Some(param), _, _, _, _, _) => param.index -> param + }.toMap + } +} \ No newline at end of file diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanIntrospector.scala b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanIntrospector.scala index e3d56c8d0..761b3b970 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanIntrospector.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/BeanIntrospector.scala @@ -58,13 +58,30 @@ object BeanIntrospector { val primaryConstructor = c.getConstructors.headOption val debugCtorParamNames = primaryConstructor.toIndexedSeq.flatMap(getCtorParams) val index = debugCtorParamNames.indexOf(name) + val companion = findCompanionObject(c) if (index >= 0) { - Some(ConstructorParameter(primaryConstructor.get, index, None)) + Some(ConstructorParameter(primaryConstructor.get, index, findConstructorDefaultValue(companion, index))) } else { findConstructorParam(c.getSuperclass, name) } } + def findConstructorDefaultValue(maybeCompanion: Option[AnyRef], index: Int): Option[() => AnyRef] = { + val methodName = "$lessinit$greater$default$" + (index + 1) + maybeCompanion.flatMap(companion => companion.getClass.getMethods.toStream.collectFirst { + case method if method.getName == methodName && method.getParameterCount == 0 => + () => method.invoke(companion) + }) + } + + def findCompanionObject(c: Class[_]): Option[AnyRef] = { + try { + Some(c.getClassLoader.loadClass(c.getName + "$").getDeclaredField("MODULE$").get(null)) + } catch { + case e: Exception => None + } + } + val hierarchy: Seq[Class[_]] = { @tailrec def next(c: Class[_], acc: List[Class[_]]): List[Class[_]] = { diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/PropertyDescriptor.scala b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/PropertyDescriptor.scala index 68e22269f..aaca4a23c 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/PropertyDescriptor.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/PropertyDescriptor.scala @@ -7,7 +7,7 @@ import com.fasterxml.jackson.module.scala.util.Implicits._ import scala.language.existentials -case class ConstructorParameter(constructor: Constructor[_], index: Int, defaultValueMethod: Option[Method]) +case class ConstructorParameter(constructor: Constructor[_], index: Int, defaultValue: Option[() => AnyRef]) case class PropertyDescriptor(name: String, param: Option[ConstructorParameter], diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/ScalaAnnotationIntrospectorModule.scala b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/ScalaAnnotationIntrospectorModule.scala index 3f647ebc0..2dd1d9316 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/introspect/ScalaAnnotationIntrospectorModule.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/introspect/ScalaAnnotationIntrospectorModule.scala @@ -3,14 +3,17 @@ package com.fasterxml.jackson.module.scala.introspect import java.lang.annotation.Annotation import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.databind.{BeanDescription, DeserializationConfig, DeserializationContext} import com.fasterxml.jackson.databind.`type`.ClassKey +import com.fasterxml.jackson.databind.deser.{CreatorProperty, NullValueProvider, SettableBeanProperty, ValueInstantiator, ValueInstantiators} +import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator import com.fasterxml.jackson.databind.introspect._ -import com.fasterxml.jackson.databind.util.LRUMap +import com.fasterxml.jackson.databind.util.{AccessPattern, LRUMap} import com.fasterxml.jackson.module.paranamer.ParanamerAnnotationIntrospector import com.fasterxml.jackson.module.scala.JacksonModule import com.fasterxml.jackson.module.scala.util.Implicits._ -object ScalaAnnotationIntrospector extends NopAnnotationIntrospector +object ScalaAnnotationIntrospector extends NopAnnotationIntrospector with ValueInstantiators { private [this] val _descriptorCache = new LRUMap[ClassKey, BeanDescriptor](16, 100) @@ -114,9 +117,54 @@ object ScalaAnnotationIntrospector extends NopAnnotationIntrospector JsonCreator.Mode.PROPERTIES } else null } + + class ScalaValueInstantiator(delegate: StdValueInstantiator, config: DeserializationConfig, descriptor: BeanDescriptor) extends StdValueInstantiator(delegate) { + + private val overriddenConstructorArguments = { + val args = delegate.getFromObjectArguments(config) + + args.map { + 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 _ => creator + } + case other => other + } + } + + override def getFromObjectArguments(config: DeserializationConfig): Array[SettableBeanProperty] = { + overriddenConstructorArguments + } + } + + override def findValueInstantiator(config: DeserializationConfig, beanDesc: BeanDescription, + defaultInstantiator: ValueInstantiator): ValueInstantiator = { + + if (isMaybeScalaBeanType(beanDesc.getBeanClass)) { + + val descriptor = _descriptorFor(beanDesc.getBeanClass) + if (descriptor.properties.exists(_.param.exists(_.defaultValue.isDefined))) { + defaultInstantiator match { + case std: StdValueInstantiator => + new ScalaValueInstantiator(std, config, descriptor) + case other => + throw new IllegalArgumentException("Cannot customise a non StdValueInstantiatiator: " + other.getClass) + } + } else defaultInstantiator + + } else defaultInstantiator + } } trait ScalaAnnotationIntrospectorModule extends JacksonModule { this += { _.appendAnnotationIntrospector(new ParanamerAnnotationIntrospector()) } this += { _.appendAnnotationIntrospector(ScalaAnnotationIntrospector) } + this += { _.addValueInstantiators(ScalaAnnotationIntrospector) } } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/CreatorTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/CreatorTest.scala index 6c5dd8beb..8812c3cf6 100755 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/CreatorTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/CreatorTest.scala @@ -38,8 +38,11 @@ object CreatorTest this(script, 0) } } + + case class ConstructorWithDefaultValues(s: String = "some string", i: Int = 10, dummy: String) } + @RunWith(classOf[JUnitRunner]) class CreatorTest extends DeserializationFixture { import CreatorTest._ @@ -104,4 +107,14 @@ class CreatorTest extends DeserializationFixture { val roundTrip = f.readValue[MultipleConstructorsAnn](bean) roundTrip shouldEqual orig } + + it should "support default values" in { f => + val deser = f.readValue[ConstructorWithDefaultValues]("""{}""") + deser.s shouldEqual "some string" + deser.i shouldEqual 10 + deser.dummy shouldEqual null + val deser2 = f.readValue[ConstructorWithDefaultValues]("""{"s":"passed","i":5}""") + deser2.s shouldEqual "passed" + deser2.i shouldEqual 5 + } }