diff --git a/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala b/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala index a383419..8c94eab 100644 --- a/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala +++ b/src/main/scala/com/codahale/jerkson/deser/CaseClassDeserializer.scala @@ -18,7 +18,7 @@ class CaseClassDeserializer(config: DeserializationConfig, classLoader: ClassLoader) extends JsonDeserializer[Object] { private val isSnakeCase = javaType.getRawClass.isAnnotationPresent(classOf[JsonSnakeCase]) private val params = CaseClassSigParser.parse(javaType.getRawClass, config.getTypeFactory, classLoader).map { - case (name, jt) => (if (isSnakeCase) snakeCase(name) else name, jt) + case (name, jt, defaultValue) => (if (isSnakeCase) snakeCase(name) else name, jt, defaultValue) }.toArray private val paramTypes = params.map { _._2.getRawClass }.toList private val constructor = javaType.getRawClass.getConstructors.find { c => @@ -51,7 +51,7 @@ class CaseClassDeserializer(config: DeserializationConfig, val node = jp.readValueAsTree val values = new ArrayBuffer[AnyRef] - for ((paramName, paramType) <- params) { + for ((paramName, paramType, paramDefault) <- params) { val field = node.get(paramName) val tp = new TreeTraversingParser(if (field == null) NullNode.getInstance else field, jp.getCodec) val value = if (paramType.getRawClass == classOf[Option[_]]) { @@ -63,9 +63,14 @@ class CaseClassDeserializer(config: DeserializationConfig, if (field != null || value != null) { values += value + } else { + // see if a default value was supplied + paramDefault match { + case Some(v) => values += v + case None => + } } - if (values.size == params.size) { return constructor.newInstance(values.toArray: _*).asInstanceOf[Object] } diff --git a/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala b/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala index 3895ea8..5b56ea1 100644 --- a/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala +++ b/src/main/scala/com/codahale/jerkson/util/CaseClassSigParser.scala @@ -51,6 +51,18 @@ object CaseClassSigParser { protected def simpleName(klass: Class[_]) = klass.getName.split("\\$").last + implicit def class2companion(clazz: Class[_]) = new { + def companionClass(classLoader: ClassLoader): Class[_] = { + val path = if (clazz.getName.endsWith("$")) clazz.getName else "%s$".format(clazz.getName) + Some(Class.forName(path, true, classLoader)).getOrElse { + throw new Error("Could not resolve clazz='%s'". + format(path)) + } + } + + def companionObject(classLoader: ClassLoader) = companionClass(classLoader).getField("MODULE$").get(null) + } + protected def findSym[A](clazz: Class[A], classLoader: ClassLoader) = { val name = simpleName(clazz) val pss = parseScalaSig(clazz, classLoader) @@ -81,12 +93,26 @@ object CaseClassSigParser { def parse[A](clazz: Class[A], factory: TypeFactory, classLoader: ClassLoader) = { findSym(clazz, classLoader).children.filter(c => c.isCaseAccessor && !c.isPrivate) - .flatMap { ms => + .zipWithIndex.map { case (ms,idx) => { ms.asInstanceOf[MethodSymbol].infoType match { - case NullaryMethodType(t: TypeRefType) => ms.name -> typeRef2JavaType(t, factory, classLoader) :: Nil + case NullaryMethodType(t: TypeRefType) => { + + // try and find the field's default + val companionClass = clazz.companionClass(classLoader) + val companionObject = clazz.companionObject(classLoader) + val defaultMethod = try { + Some(companionClass.getMethod("apply$default$%d".format(idx + 1))) + } + catch { + case _ => None // indicates no default value was supplied + } + val defaultValue = defaultMethod.map(m => Some(m.invoke(companionObject))).getOrElse(None) + + Tuple3(ms.name, typeRef2JavaType(t, factory, classLoader), defaultValue) :: Nil + } case _ => Nil } - } + }}.flatten } protected def typeRef2JavaType(ref: TypeRefType, factory: TypeFactory, classLoader: ClassLoader): JavaType = { diff --git a/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala b/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala index 7a562f5..0786394 100644 --- a/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala +++ b/src/test/scala/com/codahale/jerkson/tests/CaseClassSupportSpec.scala @@ -27,6 +27,13 @@ class CaseClassSupportSpec extends Spec { } } + class `A case class with a default field` { + @Test def `is parsable from an incomplete JSON object` = { + parse[CaseClassWithDefaultString]("""{"id":1}""").must(be(CaseClassWithDefaultString(1, "Coda"))) + parse[CaseClassWithDefaultInt]("""{"id":1}""").must(be(CaseClassWithDefaultInt(1, 42))) + } + } + class `A case class with lazy fields` { @Test def `generates a JSON object with those fields evaluated` = { generate(CaseClassWithLazyVal(1)).must(be("""{"id":1,"woo":"yeah"}""")) diff --git a/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala b/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala index c3b666f..ebefbb2 100644 --- a/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala +++ b/src/test/scala/com/codahale/jerkson/tests/ExampleCaseClasses.scala @@ -6,6 +6,9 @@ import com.codahale.jerkson.JsonSnakeCase case class CaseClass(id: Long, name: String) +case class CaseClassWithDefaultString(id: Long, name: String = "Coda") +case class CaseClassWithDefaultInt(id: Long, answer: Int = 42) + case class CaseClassWithLazyVal(id: Long) { lazy val woo = "yeah" }