diff --git a/src/main/scala/jacks/case.scala b/src/main/scala/jacks/case.scala index 912eabf..5b2083a 100644 --- a/src/main/scala/jacks/case.scala +++ b/src/main/scala/jacks/case.scala @@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer import java.lang.reflect.{Constructor, Method} -class CaseClassSerializer(t: JavaType, accessors: Array[Accessor]) extends StdSerializer[Product](t) { +class CaseClassSerializer(t: JavaType, accessors: Array[Accessor], skipNulls: Boolean) extends StdSerializer[Product](t) { override def serialize(value: Product, g: JsonGenerator, p: SerializerProvider) { g.writeStartObject() @@ -28,19 +28,34 @@ class CaseClassSerializer(t: JavaType, accessors: Array[Accessor]) extends StdSe } @inline final def include(a: Accessor, s: JsonSerializer[AnyRef], v: AnyRef): Boolean = a.include match { - case ALWAYS => true - case NON_DEFAULT => default(a) != v - case NON_EMPTY => !s.isEmpty(v) - case NON_NULL => v != null + case Some(ALWAYS) => true + case Some(NON_DEFAULT) => default(a) != v + case Some(NON_EMPTY) => !s.isEmpty(v) + case Some(NON_NULL) => v != null + case None => { + // unfortunately, while Jackson serializers have an isEmpty() to match the getEmptyValue() of deserializers, + // they lack an isNull() to match the getNullValue() of deserializers. + // We cannot use isEmpty, as otherwise empty iterables would also match. Therefore, the only + // viable solution here is to match Option's None explicitly, unfortunately. + // + // null or None should /not/ be skipped it there is an explicit default, unless the default + // happens to be null or None. But do not skip other values just because they match the default. + !skipNulls || (v != null && v != None) || (!hasNoDefault(a) && { + val df=default(a) + (df != null && df != None) + }) + } } @inline final def default(a: Accessor) = a.default match { case Some(m) => m.invoke(null) case None => null } + + @inline final def hasNoDefault(a: Accessor) = a.default == None } -class CaseClassDeserializer(t: JavaType, c: Creator) extends JsonDeserializer[Any] { +class CaseClassDeserializer(t: JavaType, c: Creator, checkNulls: Boolean) extends JsonDeserializer[Any] { val fields = c.accessors.map(a => a.name -> None).toMap[String, Option[Object]] val types = c.accessors.map(a => a.name -> a.`type`).toMap @@ -70,7 +85,28 @@ class CaseClassDeserializer(t: JavaType, c: Creator) extends JsonDeserializer[An val params = c.accessors.map { a => values(a.name) match { case Some(v) => v - case None => c.default(a) + case None => { + if (checkNulls) { + // refuse to store nulls into case class fields, unless + // explicitly requested with a default value + // In case of Option, a missing property becomes None + + if (c.hasNoDefault(a)) { + val d = ctx.findContextualValueDeserializer(a.`type`, null) + val e = d.getNullValue + if (e != null) e else + throw ctx.mappingException("Required property '"+a.name+"' is missing.") + } else { + // c hasDefault(a), hence return the default + c.default(a) + } + } else { + // Jacks 2.1.4 behavior will use c.default(). + // default() should use d.getNullValue, but instead + // always returns null (even for Option) + c.default(a) + } + } } } @@ -82,6 +118,7 @@ trait Creator { val accessors: Array[Accessor] def apply(args: Seq[AnyRef]): Any def default(a: Accessor): AnyRef + def hasNoDefault(a: Accessor) = a.default == None } class ConstructorCreator(c: Constructor[_], val accessors: Array[Accessor]) extends Creator { diff --git a/src/main/scala/jacks/jacks.scala b/src/main/scala/jacks/jacks.scala index add6430..7b5f4c7 100644 --- a/src/main/scala/jacks/jacks.scala +++ b/src/main/scala/jacks/jacks.scala @@ -10,9 +10,9 @@ import scala.collection.JavaConversions.JConcurrentMapWrapper import java.io._ import java.util.concurrent.ConcurrentHashMap -trait JacksMapper { +class JacksMapper private (options:JacksOptions) { val mapper = new ObjectMapper - mapper.registerModule(new ScalaModule) + mapper.registerModule(new ScalaModule(options)) def readValue[T: Manifest](src: Array[Byte]): T = mapper.readValue(src, resolve) def readValue[T: Manifest](src: InputStream): T = mapper.readValue(src, resolve) @@ -37,4 +37,6 @@ trait JacksMapper { }) } -object JacksMapper extends JacksMapper +object JacksMapper extends JacksMapper(JacksOptions.defaults) { + def withOptions(opts:JacksOption*) = new JacksMapper(JacksOptions(opts:_*)) +} diff --git a/src/main/scala/jacks/jacksOptions.scala b/src/main/scala/jacks/jacksOptions.scala new file mode 100644 index 0000000..f390b1c --- /dev/null +++ b/src/main/scala/jacks/jacksOptions.scala @@ -0,0 +1,20 @@ +// Copyright (C) 2011 - Will Glozer. All rights reserved. + +package com.lambdaworks.jacks + +sealed abstract class JacksOption +object JacksOption { + case class CaseClassCheckNulls(enabled:Boolean) extends JacksOption + case class CaseClassSkipNulls(enabled:Boolean) extends JacksOption +} + + +private[jacks] class JacksOptions(opts:Seq[JacksOption]=Seq.empty) { + def caseClassCheckNulls=opts contains JacksOption.CaseClassCheckNulls(true) + def caseClassSkipNulls =opts contains JacksOption.CaseClassSkipNulls(true) +} +private[jacks] object JacksOptions { + def apply(opts:JacksOption*) = + new JacksOptions(opts.groupBy(_.getClass()).toSeq.map(_._2.last)) + def defaults=new JacksOptions() +} diff --git a/src/main/scala/jacks/module.scala b/src/main/scala/jacks/module.scala index 5bc5ce9..9e7e184 100644 --- a/src/main/scala/jacks/module.scala +++ b/src/main/scala/jacks/module.scala @@ -21,17 +21,17 @@ import java.lang.reflect.{Constructor, Method} import tools.scalap.scalax.rules.scalasig.ScalaSig -class ScalaModule extends Module { +class ScalaModule(options:JacksOptions) extends Module { def version = new Version(2, 1, 0, null, "com.lambdaworks", "jacks") def getModuleName = "ScalaModule" def setupModule(ctx: Module.SetupContext) { - ctx.addSerializers(new ScalaSerializers) - ctx.addDeserializers(new ScalaDeserializers) + ctx.addSerializers(new ScalaSerializers(options)) + ctx.addDeserializers(new ScalaDeserializers(options)) } } -class ScalaDeserializers extends Deserializers.Base { +class ScalaDeserializers(options:JacksOptions) extends Deserializers.Base { override def findBeanDeserializer(t: JavaType, cfg: DeserializationConfig, bd: BeanDescription): JsonDeserializer[_] = { val cls = t.getRawClass @@ -76,8 +76,9 @@ class ScalaDeserializers extends Deserializers.Base { new TupleDeserializer(t) } else if (classOf[Product].isAssignableFrom(cls)) { ScalaTypeSig(cfg.getTypeFactory, t) match { - case Some(sts) if sts.isCaseClass => new CaseClassDeserializer(t, sts.creator) - case _ => null + case Some(sts) if sts.isCaseClass => + new CaseClassDeserializer(t, sts.creator, options.caseClassCheckNulls) + case _ => null } } else if (classOf[Symbol].isAssignableFrom(cls)) { new SymbolDeserializer @@ -116,7 +117,7 @@ class ScalaDeserializers extends Deserializers.Base { } } -class ScalaSerializers extends Serializers.Base { +class ScalaSerializers(options:JacksOptions) extends Serializers.Base { override def findSerializer(cfg: SerializationConfig, t: JavaType, bd: BeanDescription): JsonSerializer[_] = { val cls = t.getRawClass @@ -132,8 +133,9 @@ class ScalaSerializers extends Serializers.Base { new TupleSerializer(t) } else if (classOf[Product].isAssignableFrom(cls)) { ScalaTypeSig(cfg.getTypeFactory, t) match { - case Some(sts) if sts.isCaseClass => new CaseClassSerializer(t, sts.annotatedAccessors) - case _ => null + case Some(sts) if sts.isCaseClass => + new CaseClassSerializer(t, sts.annotatedAccessors, options.caseClassSkipNulls) + case _ => null } } else if (classOf[Symbol].isAssignableFrom(cls)) { new SymbolSerializer(t) @@ -150,7 +152,7 @@ case class Accessor( `type`: JavaType, default: Option[Method], ignored: Boolean = false, - include: Include = ALWAYS + include: Option[Include] = None ) class ScalaTypeSig(val tf: TypeFactory, val `type`: JavaType, val sig: ScalaSig) { @@ -199,7 +201,7 @@ class ScalaTypeSig(val tf: TypeFactory, val `type`: JavaType, val sig: ScalaSig) annotations.foldLeft(accessor) { case (accessor, a:JsonProperty) if a.value != "" => accessor.copy(name = a.value) case (accessor, a:JsonIgnore) => accessor.copy(ignored = a.value) - case (accessor, a:JsonInclude) => accessor.copy(include = a.value) + case (accessor, a:JsonInclude) => accessor.copy(include = Some(a.value)) case (accessor, _) => accessor } }.toArray @@ -211,7 +213,7 @@ class ScalaTypeSig(val tf: TypeFactory, val `type`: JavaType, val sig: ScalaSig) val ignored = ignore.value.toSet accessors.map(a => a.copy(ignored = ignored.contains(a.name))) case (accessors, include: JsonInclude) => - accessors.map(a => a.copy(include = include.value)) + accessors.map(a => a.copy(include = Some(include.value))) case (accessors, _) => accessors }