diff --git a/build.sbt b/build.sbt index d4d744957..06f58a881 100644 --- a/build.sbt +++ b/build.sbt @@ -7,7 +7,7 @@ organization := "com.fasterxml.jackson.module" scalaVersion := "2.11.8" -crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-M4") +crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-M4) scalacOptions ++= Seq("-deprecation", "-unchecked", "-feature") @@ -35,16 +35,20 @@ scalacOptions ++= ( } ) +val jacksonVersion = "2.7.3" + +val jacksonSnapshot = "2.7.4-SNAPSHOT" + libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "com.fasterxml.jackson.core" % "jackson-core" % "2.7.2", - "com.fasterxml.jackson.core" % "jackson-annotations" % "2.7.2", - "com.fasterxml.jackson.core" % "jackson-databind" % "2.7.2", - "com.fasterxml.jackson.module" % "jackson-module-paranamer" % "2.7.2", + "com.fasterxml.jackson.core" % "jackson-core" % jacksonVersion, + "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion, + "com.fasterxml.jackson.core" % "jackson-databind" % jacksonSnapshot, + "com.fasterxml.jackson.module" % "jackson-module-paranamer" % jacksonVersion, // test dependencies - "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % "2.7.2" % "test", - "com.fasterxml.jackson.datatype" % "jackson-datatype-guava" % "2.7.2" % "test", - "com.fasterxml.jackson.module" % "jackson-module-jsonSchema" % "2.7.2" % "test", + "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % jacksonVersion % "test", + "com.fasterxml.jackson.datatype" % "jackson-datatype-guava" % jacksonVersion % "test", + "com.fasterxml.jackson.module" % "jackson-module-jsonSchema" % jacksonVersion % "test", "org.scalatest" %% "scalatest" % "2.2.6" % "test", "junit" % "junit" % "4.11" % "test" ) diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerModule.scala b/src/main/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerModule.scala index e2cc632cc..d1495ba5a 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerModule.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerModule.scala @@ -1,83 +1,95 @@ package com.fasterxml.jackson.module.scala.deser -import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.{JsonToken, JsonParser} import com.fasterxml.jackson.databind._ -import com.fasterxml.jackson.databind.`type`.CollectionLikeType +import com.fasterxml.jackson.databind.`type`.{ReferenceType, TypeFactory, CollectionLikeType} import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.deser.{ContextualDeserializer, Deserializers} import com.fasterxml.jackson.databind.jsontype.TypeDeserializer import com.fasterxml.jackson.module.scala.modifiers.OptionTypeModifierModule -private class OptionDeserializer(elementType: JavaType, - valueTypeDeser: Option[TypeDeserializer], - beanProperty: Option[BeanProperty], - elementDeser: Option[JsonDeserializer[_]]) - extends StdDeserializer[Option[AnyRef]](classOf[Option[AnyRef]]) with ContextualDeserializer { - - override def createContextual(ctxt: DeserializationContext, property: BeanProperty): JsonDeserializer[_] = { - val typeDeser = valueTypeDeser.map(_.forProperty(property)) - val deser: Option[JsonDeserializer[_]] = - (for { - p <- Option(property) - m <- Option(p.getMember) - deserDef <- Option(ctxt.getAnnotationIntrospector.findContentDeserializer(m)) - } yield ctxt.deserializerInstance(m, deserDef)).orElse(elementDeser) - val deser1: Option[JsonDeserializer[_]] = Option(findConvertingContentDeserializer(ctxt, property, deser.orNull)) - val deser2: Option[JsonDeserializer[_]] = if (deser1.isEmpty) { - if (hasContentTypeAnnotation(ctxt, property)) { - Option(ctxt.findContextualValueDeserializer(elementType, property)) - } else { - deser1 - } - } else { - Option(ctxt.handleSecondaryContextualization(deser1.get, property, elementType)) +private class OptionDeserializer(fullType: JavaType, + valueTypeDeserializer: Option[TypeDeserializer], + valueDeserializer: Option[JsonDeserializer[AnyRef]], + beanProperty: Option[BeanProperty] = None) + extends StdDeserializer[Option[AnyRef]](fullType) with ContextualDeserializer { + + override def getValueType: JavaType = fullType + + override def getNullValue: Option[AnyRef] = None + + private[this] def withResolved(fullType: JavaType, + typeDeser: Option[TypeDeserializer], + valueDeser: Option[JsonDeserializer[_]], + beanProperty: Option[BeanProperty]): OptionDeserializer = { + if (fullType == this.fullType && + typeDeser == this.valueTypeDeserializer && + valueDeser == this.valueDeserializer && + beanProperty == this.beanProperty) { + return this } - if (deser2 != elementDeser || property != beanProperty.orNull || valueTypeDeser != typeDeser) - new OptionDeserializer(elementType, typeDeser, Option(property), deser2.asInstanceOf[Option[JsonDeserializer[AnyRef]]]) - else this + new OptionDeserializer(fullType, typeDeser, valueDeser.asInstanceOf[Option[JsonDeserializer[AnyRef]]], beanProperty) } - def hasContentTypeAnnotation(ctxt: DeserializationContext, property: BeanProperty) = (for { - p <- Option(property) - intr <- Option(ctxt.getAnnotationIntrospector) - } yield { - intr.refineDeserializationType(ctxt.getConfig, p.getMember, p.getType) - }).isDefined + override def createContextual(ctxt: DeserializationContext, property: BeanProperty): JsonDeserializer[Option[AnyRef]] = { + val typeDeser = valueTypeDeserializer.map(_.forProperty(property)) + var deser = valueDeserializer + var typ = fullType + + def refdType() = Option(typ.getContentType).getOrElse(TypeFactory.unknownType()) - override def deserialize(jp: JsonParser, ctxt: DeserializationContext) = valueTypeDeser match { - case Some(d) => deserializeWithType(jp, ctxt, d) - case None => Option { - elementDeser.map(_.deserialize(jp, ctxt)).getOrElse { - ctxt.findContextualValueDeserializer(elementType, beanProperty.orNull).deserialize(jp, ctxt) + if (deser.isEmpty) { + if (property != null) { + val intr = ctxt.getAnnotationIntrospector + val member = property.getMember + if (intr != null && member != null) { + typ = intr.refineDeserializationType(ctxt.getConfig, member, typ) + } + deser = Option(ctxt.findContextualValueDeserializer(refdType(), property)) } - }.asInstanceOf[Option[AnyRef]] + } else { // otherwise directly assigned, probably not contextual yet: + deser = Option(ctxt.handleSecondaryContextualization(deser.get, property, refdType()).asInstanceOf[JsonDeserializer[AnyRef]]) + } + + withResolved(typ, typeDeser, deser, Option(property)) } - override def deserializeWithType(jp: JsonParser, ctxt: DeserializationContext, typeDeserializer: TypeDeserializer) = Option { - elementDeser.map(_.deserializeWithType(jp, ctxt, typeDeserializer)).getOrElse { - ctxt.findContextualValueDeserializer(elementType, beanProperty.orNull).deserializeWithType(jp, ctxt, typeDeserializer) + override def deserialize(p: JsonParser, ctxt: DeserializationContext): Option[AnyRef] = { + val deser = valueDeserializer.getOrElse(ctxt.findContextualValueDeserializer(fullType.getContentType, beanProperty.orNull)) + val refd: AnyRef = valueTypeDeserializer match { + case None => deser.deserialize(p, ctxt) + case Some(vtd) => deser.deserializeWithType(p, ctxt, vtd) } + Option(refd) } - override def getNullValue = None + override def deserializeWithType(jp: JsonParser, ctxt: DeserializationContext, typeDeserializer: TypeDeserializer): Option[AnyRef] = { + val t = jp.getCurrentToken + if (t == JsonToken.VALUE_NULL) { + getNullValue(ctxt) + } else { + typeDeserializer.deserializeTypedFromAny(jp, ctxt).asInstanceOf[Option[AnyRef]] + } + } } private object OptionDeserializerResolver extends Deserializers.Base { private val OPTION = classOf[Option[AnyRef]] - override def findCollectionLikeDeserializer(theType: CollectionLikeType, - config: DeserializationConfig, - beanDesc: BeanDescription, - elementTypeDeserializer: TypeDeserializer, - elementValueDeserializer: JsonDeserializer[_]) = - if (!OPTION.isAssignableFrom(theType.getRawClass)) null + override def findReferenceDeserializer(refType: ReferenceType, + config: DeserializationConfig, + beanDesc: BeanDescription, + contentTypeDeserializer: TypeDeserializer, + contentDeserializer: JsonDeserializer[_]): JsonDeserializer[_] = { + if (!OPTION.isAssignableFrom(refType.getRawClass)) null else { - val elementType = theType.getContentType - val typeDeser = Option(elementTypeDeserializer).orElse(Option(elementType.getTypeHandler.asInstanceOf[TypeDeserializer])) - val valDeser: Option[JsonDeserializer[_]] = Option(elementValueDeserializer).orElse(Option(elementType.getValueHandler)) - new OptionDeserializer(elementType, typeDeser, None, valDeser) + val elementType = refType.getContentType + val typeDeser = Option(contentTypeDeserializer).orElse(Option(elementType.getTypeHandler[TypeDeserializer])) + val valDeser = Option(contentDeserializer).orElse(Option(elementType.getValueHandler)).asInstanceOf[Option[JsonDeserializer[AnyRef]]] + new OptionDeserializer(refType, typeDeser, valDeser) } + } } trait OptionDeserializerModule extends OptionTypeModifierModule { diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapper.scala b/src/main/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapper.scala index 74fd5261f..2fb6d90e7 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapper.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapper.scala @@ -72,6 +72,12 @@ trait ScalaObjectMapper { throw new IllegalArgumentException("Need exactly 1 type parameter for collection like types ("+clazz.getName+")") } getTypeFactory.constructCollectionLikeType(clazz, typeArguments(0)) + } else if (isReference(clazz)) { + val typeArguments = m.typeArguments.map(constructType(_)).toArray + if(typeArguments.length != 1) { + throw new IllegalArgumentException("Need exactly 1 type parameter for reference types ("+clazz.getName+")") + } + getTypeFactory.constructReferenceType(clazz, typeArguments(0)) } else { val typeArguments = m.typeArguments.map(constructType(_)).toArray getTypeFactory.constructParametrizedType(clazz, clazz, typeArguments: _*) @@ -333,10 +339,14 @@ trait ScalaObjectMapper { MAP.isAssignableFrom(c) } - private val ITERABLE = classOf[collection.Iterable[_]] private val OPTION = classOf[Option[_]] + private def isReference(c: Class[_]): Boolean = { + OPTION.isAssignableFrom(c) + } + + private val ITERABLE = classOf[collection.Iterable[_]] private def isCollectionLike(c: Class[_]): Boolean = { - ITERABLE.isAssignableFrom(c) || OPTION.isAssignableFrom(c) + ITERABLE.isAssignableFrom(c) } } diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/modifiers/OptionTypeModifierModule.scala b/src/main/scala/com/fasterxml/jackson/module/scala/modifiers/OptionTypeModifierModule.scala index d5b471883..5273d9fd8 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/modifiers/OptionTypeModifierModule.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/modifiers/OptionTypeModifierModule.scala @@ -1,11 +1,23 @@ package com.fasterxml.jackson.module.scala.modifiers +import java.lang.reflect.Type + +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.`type`.{ReferenceType, TypeBindings, TypeFactory, TypeModifier} import com.fasterxml.jackson.module.scala.JacksonModule -private object OptionTypeModifier extends CollectionLikeTypeModifier { - def BASE = classOf[Option[Any]] +private object OptionTypeModifier extends TypeModifier with GenTypeModifier { + def OPTION = classOf[Option[AnyRef]] + + override def modifyType(typ: JavaType, jdkType: Type, context: TypeBindings, typeFactory: TypeFactory): JavaType = { + if (typ.isReferenceType || typ.isContainerType) return typ + + if (classObjectFor(jdkType).exists(OPTION.isAssignableFrom)) { + ReferenceType.upgradeFrom(typ, typ.containedTypeOrUnknown(0)) + } else typ + } } trait OptionTypeModifierModule extends JacksonModule { this += OptionTypeModifier -} \ No newline at end of file +} diff --git a/src/main/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerModule.scala b/src/main/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerModule.scala index 57ec75495..036bf6a5e 100644 --- a/src/main/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerModule.scala +++ b/src/main/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerModule.scala @@ -2,115 +2,171 @@ package com.fasterxml.jackson package module.scala package ser -import util.Implicits._ -import modifiers.OptionTypeModifierModule - -import core.JsonGenerator -import databind._ -import jsontype.TypeSerializer -import jsonschema.{JsonSchema, SchemaAware} -import ser.{ContextualSerializer, BeanPropertyWriter, BeanSerializerModifier, Serializers} -import ser.impl.UnknownSerializer -import ser.std.StdSerializer -import com.fasterxml.jackson.databind.`type`.CollectionLikeType -import jsonFormatVisitors.JsonFormatVisitorWrapper - import java.lang.reflect.Type -import java.{util => ju} +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind._ +import com.fasterxml.jackson.databind.`type`.ReferenceType +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper +import com.fasterxml.jackson.databind.jsonschema.{JsonSchema, SchemaAware} +import com.fasterxml.jackson.databind.jsontype.TypeSerializer +import com.fasterxml.jackson.databind.ser.impl.{PropertySerializerMap, UnknownSerializer} +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.fasterxml.jackson.databind.ser.{ContextualSerializer, Serializers} +import com.fasterxml.jackson.databind.util.NameTransformer +import com.fasterxml.jackson.module.scala.modifiers.OptionTypeModifierModule +import com.fasterxml.jackson.module.scala.util.Implicits._ + +private object OptionSerializer { + def useStatic(provider: SerializerProvider, property: Option[BeanProperty], referredType: JavaType): Boolean = { + // First: no serializer for `Object.class`, must be dynamic + if (referredType.isJavaLangObject) return false + // but if type is final, might as well fetch + if (referredType.isFinal) return true + // also: if indicated by typing, should be considered static + if (referredType.useStaticType()) return true + // if neither, maybe explicit annotation? + for ( + ann <- property.flatMap(p => Option(p.getMember)); + intr <- Option(provider.getAnnotationIntrospector) + ) { + val typing = intr.findSerializationTyping(ann) + if (typing == JsonSerialize.Typing.STATIC) return true + if (typing == JsonSerialize.Typing.DYNAMIC) return false + } + // and finally, may be forced by global static typing (unlikely...) + provider.isEnabled(MapperFeature.USE_STATIC_TYPING) + } + + def findSerializer(provider: SerializerProvider, typ: Class[_], prop: Option[BeanProperty]): JsonSerializer[AnyRef] = { + // Important: ask for TYPED serializer, in case polymorphic handling is needed! + provider.findTypedValueSerializer(typ, true, prop.orNull) + } + + def findSerializer(provider: SerializerProvider, typ: JavaType, prop: Option[BeanProperty]): JsonSerializer[AnyRef] = { + // Important: ask for TYPED serializer, in case polymorphic handling is needed! + provider.findTypedValueSerializer(typ, true, prop.orNull) + } + + def hasContentTypeAnnotation(provider: SerializerProvider, property: BeanProperty): Boolean = { + val intr = provider.getAnnotationIntrospector + if (property == null || intr == null) return false + intr.refineSerializationType(provider.getConfig, property.getMember, property.getType) != null + } +} -private class OptionSerializer(elementType: Option[JavaType], +private class OptionSerializer(referredType: JavaType, + property: Option[BeanProperty], valueTypeSerializer: Option[TypeSerializer], - beanProperty: Option[BeanProperty], - elementSerializer: Option[JsonSerializer[AnyRef]]) - extends StdSerializer[Option[AnyRef]](classOf[Option[AnyRef]]) - with ContextualSerializer - with SchemaAware -{ - override def serialize(value: Option[AnyRef], jgen: JsonGenerator, provider: SerializerProvider) { - def doSerialize(v: AnyRef) = { - val cls = v.getClass - val ser = elementType.filter(_.hasGenericTypes) match { - case Some(et) => provider.findValueSerializer(provider.constructSpecializedType(et, cls), beanProperty.orNull) - case None => provider.findValueSerializer(cls, beanProperty.orNull) - } - ser.serialize(v, jgen, provider) - } + valueSerializer: Option[JsonSerializer[AnyRef]], + contentInclusion: Option[JsonInclude.Include], + unwrapper: Option[NameTransformer], + var dynamicSerializers: PropertySerializerMap = PropertySerializerMap.emptyForProperties()) + extends StdSerializer[Option[AnyRef]](referredType) + with ContextualSerializer + with SchemaAware { + + import OptionSerializer._ + + override def unwrappingSerializer(transformer: NameTransformer): JsonSerializer[Option[AnyRef]] = { + val ser = valueSerializer.map(_.unwrappingSerializer(transformer)) + val unt = unwrapper.map(NameTransformer.chainedTransformer(transformer, _)).getOrElse(transformer) + withResolved(property, valueTypeSerializer, ser, Option(unt), contentInclusion) + } - valueTypeSerializer match { - case Some(vt) => serializeWithType(value, jgen, provider, vt) - case None => - (value, elementSerializer) match { - case (Some(v), Some(vs: UnknownSerializer)) => doSerialize(v) - case (Some(v), None) => doSerialize(v) - case (Some(v), Some(vs)) => vs.serialize(v, jgen, provider) - case (None, _) => provider.defaultSerializeNull(jgen) - } - } + protected[this] def withResolved(prop: Option[BeanProperty], vts: Option[TypeSerializer], + valueSer: Option[JsonSerializer[AnyRef]], unt: Option[NameTransformer], + contentIncl: Option[JsonInclude.Include]): OptionSerializer = { + if (prop == property && vts == valueTypeSerializer && valueSer == valueSerializer && + contentIncl == contentInclusion && unt == unwrapper) this + else new OptionSerializer(referredType, prop, vts, valueSer, contentIncl, unt, dynamicSerializers) } - override def serializeWithType(value: Option[AnyRef], jgen: JsonGenerator, provider: SerializerProvider, typeSer: TypeSerializer) { - def doSerialize(v: AnyRef) = { - val cls = v.getClass - val ser = elementType.filter(_.hasGenericTypes) match { - case Some(et) => provider.findTypedValueSerializer(provider.constructSpecializedType(et, cls), true, beanProperty.orNull) - case None => provider.findTypedValueSerializer(cls, true, beanProperty.orNull) - } - ser.serializeWithType(v, jgen, provider, typeSer) + override def createContextual(prov: SerializerProvider, prop: BeanProperty): JsonSerializer[_] = { + val propOpt = Option(prop) + + val vts = valueTypeSerializer.optMap(_.forProperty(prop)) + var ser = for ( + prop <- propOpt; + member <- Option(prop.getMember); + serDef <- Option(prov.getAnnotationIntrospector.findContentSerializer(member)) + ) yield prov.serializerInstance(member, serDef) + ser = ser.orElse(valueSerializer).map(prov.handlePrimaryContextualization(_, prop)).asInstanceOf[Option[JsonSerializer[AnyRef]]] + ser = Option(findConvertingContentSerializer(prov, prop, ser.orNull).asInstanceOf[JsonSerializer[AnyRef]]) + ser = ser match { + case None => if (hasContentTypeAnnotation(prov, prop)) { + Option(prov.findValueSerializer(referredType, prop)).filterNot(_.isInstanceOf[UnknownSerializer]) + } else None + // Why secondary why not primary? + case Some(s) => Option(prov.handleSecondaryContextualization(s, prop).asInstanceOf[JsonSerializer[AnyRef]]) } - (value, elementSerializer) match { - case (Some(v), Some(vs: UnknownSerializer)) => doSerialize(v) - case (Some(v), None) => doSerialize(v) - case (Some(v), Some(vs)) => vs.serializeWithType(v, jgen, provider, typeSer) - case (None, _) => provider.defaultSerializeNull(jgen) + // A few conditions needed to be able to fetch serializer here: + if (ser.isEmpty && useStatic(prov, propOpt, referredType)) { + ser = Option(findSerializer(prov, referredType, propOpt)) } + // Also: may want to have more refined exclusion based on referenced value + val newIncl = propOpt match { + case None => contentInclusion + case Some(p) => + val pinc = p.findPropertyInclusion(prov.getConfig, classOf[Option[AnyRef]]) + val incl = pinc.getContentInclusion + if (incl != JsonInclude.Include.USE_DEFAULTS) { + Some(incl) + } else contentInclusion + } + withResolved(propOpt, vts, ser, unwrapper, newIncl) + } + + override def isEmpty(provider: SerializerProvider, value: Option[AnyRef]): Boolean = { + if (value == null || value.isEmpty) return true + if (contentInclusion.isEmpty) return false + val contents = value.get + valueSerializer + .getOrElse(findCachedSerializer(provider, contents.getClass)) + .isEmpty(provider, contents) } - override def createContextual(prov: SerializerProvider, property: BeanProperty): JsonSerializer[_] = { - // Based on the version in AsArraySerializerBase - val typeSer = valueTypeSerializer.optMap(_.forProperty(property)) - var ser: Option[JsonSerializer[_]] = - Option(property).flatMap { p => - Option(p.getMember).flatMap { m => - Option(prov.getAnnotationIntrospector.findContentSerializer(m)).map { serDef => - prov.serializerInstance(m, serDef) - } - } - } orElse elementSerializer - ser = Option(findConvertingContentSerializer(prov, property, ser.orNull)) - if (ser.isEmpty) { - if (elementType.isDefined) { - if (hasContentTypeAnnotation(prov, property)) { - ser = Option(prov.findValueSerializer(elementType.get, property)) - } + override def isUnwrappingSerializer: Boolean = unwrapper.isDefined + + override def serialize(opt: Option[AnyRef], gen: JsonGenerator, provider: SerializerProvider): Unit = { + if (opt.isEmpty) { + if (unwrapper.isEmpty) { + provider.defaultSerializeNull(gen) } + return } - else { - ser = Option(prov.handleSecondaryContextualization(ser.get, property)) + + val value = opt.get + val ser = valueSerializer.getOrElse(findCachedSerializer(provider, value.getClass)) + valueTypeSerializer match { + case Some(vts) => ser.serializeWithType(value, gen, provider, vts) + case None => ser.serialize(value, gen, provider) } - if ((ser != elementSerializer) || (property != beanProperty.orNull) || (valueTypeSerializer != typeSer)) - new OptionSerializer(elementType, typeSer, Option(property), ser.asInstanceOf[Option[JsonSerializer[AnyRef]]]) - else this } - def hasContentTypeAnnotation(provider: SerializerProvider, property: BeanProperty) = { - Option(property).exists { p => - Option(provider.getAnnotationIntrospector).exists { intr => - Option(intr.refineSerializationType(provider.getConfig, p.getMember, p.getType)).isDefined + override def serializeWithType(opt: Option[AnyRef], gen: JsonGenerator, provider: SerializerProvider, typeSer: TypeSerializer): Unit = { + if (opt.isEmpty) { + if (unwrapper.isEmpty) { + provider.defaultSerializeNull(gen) } + return } + // Otherwise apply type-prefix/suffix, then std serialize: + typeSer.writeTypePrefixForScalar(opt, gen, classOf[Option[_]]) + serialize(opt, gen, provider) + typeSer.writeTypeSuffixForScalar(opt, gen) } - override def isEmpty(value: Option[AnyRef]): Boolean = value.isEmpty - override def getSchema(provider: SerializerProvider, typeHint: Type): JsonNode = getSchema(provider, typeHint, isOptional = true) override def getSchema(provider: SerializerProvider, typeHint: Type, isOptional: Boolean): JsonNode = { - val contentSerializer = elementSerializer.getOrElse { + val contentSerializer = valueSerializer.getOrElse { val javaType = provider.constructType(typeHint) val componentType = javaType.getContentType - provider.findTypedValueSerializer(componentType, true, beanProperty.orNull) + provider.findTypedValueSerializer(componentType, true, property.orNull) } contentSerializer match { case cs: SchemaAware => cs.getSchema(provider, contentSerializer.handledType(), isOptional) @@ -118,36 +174,20 @@ private class OptionSerializer(elementType: Option[JavaType], } } - override def acceptJsonFormatVisitor(wrapper: JsonFormatVisitorWrapper, javaType: JavaType) { - val containedType = javaType.getContentType - val ser = elementSerializer.getOrElse(wrapper.getProvider.findTypedValueSerializer(containedType, true, beanProperty.orNull)) - ser.acceptJsonFormatVisitor(wrapper, containedType) + override def acceptJsonFormatVisitor(visitor: JsonFormatVisitorWrapper, typeHint: JavaType): Unit = { + var ser = valueSerializer.getOrElse(findSerializer(visitor.getProvider, referredType, property)) + ser = unwrapper.map(ser.unwrappingSerializer).getOrElse(ser) + ser.acceptJsonFormatVisitor(visitor, referredType) } -} -private class OptionPropertyWriter(delegate: BeanPropertyWriter) extends BeanPropertyWriter(delegate) -{ - override def serializeAsField(bean: AnyRef, jgen: JsonGenerator, prov: SerializerProvider) { - (get(bean), _nullSerializer) match { - // value is None, which we'll serialize as null, but there's no - // null-serializer, which means it should be suppressed - case (None, null) => return - case _ => super.serializeAsField(bean, jgen, prov) + protected[this] def findCachedSerializer(prov: SerializerProvider, typ: Class[_]): JsonSerializer[AnyRef] = { + var ser = dynamicSerializers.serializerFor(typ) + if (ser == null) { + ser = findSerializer(prov, typ, property) + ser = unwrapper.map(ser.unwrappingSerializer).getOrElse(ser) + dynamicSerializers = dynamicSerializers.newWith(typ, ser) } - } -} - -private object OptionBeanSerializerModifier extends BeanSerializerModifier { - - override def changeProperties(config: SerializationConfig, - beanDesc: BeanDescription, - beanProperties: ju.List[BeanPropertyWriter]): ju.List[BeanPropertyWriter] = { - import scala.collection.JavaConversions._ - beanProperties.transform(w => if (isOption(w)) new OptionPropertyWriter(w) else w) - } - - private[this] def isOption(writer: BeanPropertyWriter): Boolean = { - classOf[Option[_]].isAssignableFrom(writer.getPropertyType) + ser } } @@ -155,25 +195,21 @@ private object OptionSerializerResolver extends Serializers.Base { private val OPTION = classOf[Option[_]] - override def findCollectionLikeSerializer(config: SerializationConfig, - `type`: CollectionLikeType, - beanDesc: BeanDescription, - elementTypeSerializer: TypeSerializer , - elementValueSerializer: JsonSerializer[AnyRef] - ): JsonSerializer[_] = - - if (!OPTION.isAssignableFrom(`type`.getRawClass)) null - else { - val elementType = `type`.getContentType - val typeSer = Option(elementTypeSerializer).orElse(Option(elementType.getTypeHandler.asInstanceOf[TypeSerializer])) - val valSer = Option(elementValueSerializer).orElse(Option(elementType.getValueHandler.asInstanceOf[JsonSerializer[AnyRef]])) - new OptionSerializer(Option(`type`.getContentType), typeSer, None, valSer) - } + override def findReferenceSerializer(config: SerializationConfig, + refType: ReferenceType, + beanDesc: BeanDescription, + contentTypeSerializer: TypeSerializer, + contentValueSerializer: JsonSerializer[AnyRef]): JsonSerializer[_] = { + if (!OPTION.isAssignableFrom(refType.getRawClass)) return null + new OptionSerializer(refType.getReferencedType, property = None, + valueTypeSerializer = Option(contentTypeSerializer).orElse(Option(refType.getTypeHandler[TypeSerializer])), + valueSerializer = Option(contentValueSerializer).orElse(Option(refType.getValueHandler[JsonSerializer[AnyRef]])), + contentInclusion = None, unwrapper = None) + } } trait OptionSerializerModule extends OptionTypeModifierModule { this += { ctx => ctx addSerializers OptionSerializerResolver - ctx addBeanSerializerModifier OptionBeanSerializerModifier } } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/JacksonTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/JacksonTest.scala index 5b19221e8..804ad7152 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/JacksonTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/JacksonTest.scala @@ -3,13 +3,11 @@ package com.fasterxml.jackson.module.scala import com.fasterxml.jackson.databind.{Module, ObjectMapper} abstract class JacksonTest extends BaseSpec { - def module: Module - def mapper = { + def newMapper = { val result = new ObjectMapper result.registerModule(module) result } - } \ No newline at end of file diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/CaseClassDeserializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/CaseClassDeserializerTest.scala index 5bb570cb1..1bf91a302 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/CaseClassDeserializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/CaseClassDeserializerTest.scala @@ -115,7 +115,7 @@ class CaseClassDeserializerTest extends DeserializerTest { it should "support serializing into instance var properties" in { val bean = new Bean("ctor") - val reader: ObjectReader = mapper.readerFor(bean.getClass) + val reader: ObjectReader = newMapper.readerFor(bean.getClass) reader.withValueToUpdate(bean).readValue("""{"prop":"readValue"}""") bean.prop should be ("readValue") } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/DeserializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/DeserializerTest.scala index 12676106b..c1fad5666 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/DeserializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/DeserializerTest.scala @@ -13,10 +13,10 @@ import com.fasterxml.jackson.module.scala.JacksonTest trait DeserializerTest extends JacksonTest { - def serialize(o: AnyRef) = mapper.writeValueAsString(o) + def serialize(o: AnyRef) = newMapper.writeValueAsString(o) def deserialize[T: Manifest](value: String) : T = - mapper.readValue(value, typeReference[T]) + newMapper.readValue(value, typeReference[T]) private [this] def typeReference[T: Manifest] = new TypeReference[T] { override def getType = typeFromManifest(manifest[T]) diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/MiscTypesTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/MiscTypesTest.scala index d3e5dbaa3..223e7171f 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/MiscTypesTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/MiscTypesTest.scala @@ -15,7 +15,7 @@ class MiscTypesTest extends DeserializerTest { "Scala Module" should "deserialize UUID" in { val data: Seq[UUID] = Stream.continually(UUID.randomUUID).take(4).toList - val json = mapper.writeValueAsString(data) + val json = newMapper.writeValueAsString(data) val read = deserialize[List[UUID]](json) read shouldBe (data) diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerTest.scala index a6ff176b9..8f633ae65 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionDeserializerTest.scala @@ -56,7 +56,6 @@ class OptionDeserializerTest extends DeserializerTest { deserialize[Option[Long]]("1") should be (Some(1L)) deserialize[Option[Long]]("1").map(java.lang.Long.valueOf(_)) should be (Some(1L)) deserialize[Option[Long]]("1").get.getClass should be (classOf[Long]) - } it should "sythensize None for optional fields that are non-existent" in { @@ -73,9 +72,9 @@ class OptionDeserializerTest extends DeserializerTest { } it should "deserialze defaulted parameters correctly (without defaults)" in { - val json = mapper.writeValueAsString(Defaulted(id = 1)) + val json = newMapper.writeValueAsString(Defaulted(id = 1)) json shouldBe """{"id":1,"name":""}""" - val d = mapper.readValue(json, classOf[Defaulted]) + val d = newMapper.readValue(json, classOf[Defaulted]) d.name should not be null } } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionWithJodaTimeDeserializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionWithJodaTimeDeserializerTest.scala index 8a9739eec..c111cc995 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionWithJodaTimeDeserializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/OptionWithJodaTimeDeserializerTest.scala @@ -19,7 +19,7 @@ class OptionWithJodaTimeDeserializerTest extends DeserializerTest { } it should "deserialize a case class with Option with JodaModule" in { - mapper.registerModule(new JodaModule) + newMapper.registerModule(new JodaModule) deserialize[OptionalInt](stringValue) should be (OptionalInt(Some(123))) } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/PrimitiveContainerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/PrimitiveContainerTest.scala index f6b0c5e72..4ac8ac8d5 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/PrimitiveContainerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/PrimitiveContainerTest.scala @@ -5,8 +5,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner -object PrimitiveContainerTest -{ +object PrimitiveContainerTest { + case class OptionInt(value: Option[Int]) case class AnnotatedOptionInt(@JsonDeserialize(contentAs = classOf[java.lang.Integer]) value: Option[Int]) case class OptionLong(value: Option[Long]) diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/SortedMapDeserializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/SortedMapDeserializerTest.scala index 3ee623986..43360c673 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/SortedMapDeserializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/SortedMapDeserializerTest.scala @@ -39,7 +39,7 @@ class SortedMapDeserializerTest extends DeserializerTest { } it should "handle key type information" in { - val result: SortedMap[UUID,Int] = mapper.readValue("""{"e79bf81e-3902-4801-831f-d161be435787":5}""", new TypeReference[SortedMap[UUID,Int]]{}) + val result: SortedMap[UUID,Int] = newMapper.readValue("""{"e79bf81e-3902-4801-831f-d161be435787":5}""", new TypeReference[SortedMap[UUID,Int]]{}) result.keys.head shouldBe UUID.fromString("e79bf81e-3902-4801-831f-d161be435787") } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/TestInnerClass.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/TestInnerClass.scala index fc7a52e75..79a1fd339 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/TestInnerClass.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/TestInnerClass.scala @@ -36,7 +36,7 @@ class TestInnerClass extends DeserializerTest { "Deserializer" should "support nested inner classes as values" in { val input = Dog("Smurf", thinking = true) - val json = mapper.writeValueAsString(input) + val json = newMapper.writeValueAsString(input) val output = deserialize[Dog](json) output should have ('name ("Smurf")) diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/TupleDeserializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/TupleDeserializerTest.scala index 628c2effd..782f66327 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/TupleDeserializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/TupleDeserializerTest.scala @@ -71,14 +71,14 @@ class TupleDeserializerTest extends DeserializerTest { it should "deserialize using type information" in { val value = TupleContainer(TupleValueLong(1), TupleValueString("foo")) - val json = mapper.writeValueAsString(value) + val json = newMapper.writeValueAsString(value) val result = deserialize[TupleContainer](json) result should be (value) } it should "deserialize using type information outside of field" in { val value = (TupleValueLong(1), TupleValueString("foo")) - val json = mapper.writeValueAsString(value) + val json = newMapper.writeValueAsString(value) val result = deserialize[(TupleValueBase, TupleValueBase)](json) result should be (value) } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/deser/UnsortedMapDeserializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/deser/UnsortedMapDeserializerTest.scala index fce994035..0f592e480 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/deser/UnsortedMapDeserializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/deser/UnsortedMapDeserializerTest.scala @@ -48,7 +48,7 @@ class UnsortedMapDeserializerTest extends DeserializerTest { } it should "handle key type information" in { - val result: Map[UUID,Int] = mapper.readValue("""{"e79bf81e-3902-4801-831f-d161be435787":5}""", new TypeReference[Map[UUID,Int]]{}) + val result: Map[UUID,Int] = newMapper.readValue("""{"e79bf81e-3902-4801-831f-d161be435787":5}""", new TypeReference[Map[UUID,Int]]{}) result.keys.head shouldBe (UUID.fromString("e79bf81e-3902-4801-831f-d161be435787")) } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapperTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapperTest.scala index a98661d9a..b3606bb8c 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapperTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/experimental/ScalaObjectMapperTest.scala @@ -196,8 +196,15 @@ class ScalaObjectMapperTest extends FlatSpec with Matchers { assert(result.isInstanceOf[collection.Map[_, _]]) } - it should "read a option values from a JSON array" in { - val result = mapper.readValue[List[Option[String]]](toplevelOptionArrayJson) + it should "read option values into List from a JSON array" in { + val result = mapper.readValue[java.util.ArrayList[Option[String]]](toplevelOptionArrayJson) + import scala.collection.JavaConversions._ + result(0) should equal(Some("some")) + result(1) should equal(None) + } + + it should "read option values into Array from a JSON array" in { + val result = mapper.readValue[Array[Option[String]]](toplevelOptionArrayJson) result(0) should equal(Some("some")) result(1) should equal(None) } diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/ser/MapSerializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/ser/MapSerializerTest.scala index 0bb7d7656..2dd0cb3d5 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/ser/MapSerializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/ser/MapSerializerTest.scala @@ -118,7 +118,7 @@ class MapSerializerTest extends SerializerTest { val wrapper = new { val map = Map("key" -> None) } - val m = mapper.copy() + val m = newMapper.copy() m.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false) val v = m.writeValueAsString(wrapper) v shouldBe """{"map":{}}""" diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerTest.scala index 81ff04cf6..056431562 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/ser/OptionSerializerTest.scala @@ -1,23 +1,22 @@ package com.fasterxml.jackson.module.scala.ser -import java.util - +import com.fasterxml.jackson.annotation._ import com.fasterxml.jackson.databind.node.JsonNodeType +import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.fasterxml.jackson.module.jsonSchema.JsonSchema +import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import com.fasterxml.jackson.module.scala.experimental.{RequiredPropertiesSchemaModule, ScalaObjectMapper} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner -import com.fasterxml.jackson.module.scala.DefaultScalaModule -import com.fasterxml.jackson.annotation._ + import scala.annotation.meta.{field, getter} -import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper -import com.fasterxml.jackson.module.scala.experimental.{ScalaObjectMapper, RequiredPropertiesSchemaModule} -import com.fasterxml.jackson.databind.{ObjectMapper, JsonNode} import scala.collection.JavaConverters._ -object OptionSerializerTest -{ - class NonEmptyOptions { +import java.util +object OptionSerializerTest { + class NonEmptyOptions { //@JsonProperty @(JsonInclude)(JsonInclude.Include.NON_EMPTY) val none = None @@ -25,12 +24,13 @@ object OptionSerializerTest //@JsonProperty @(JsonInclude @getter)(JsonInclude.Include.NON_EMPTY) val some = Some(1) - } + case class OptionGeneric[T](data: Option[T]) + case class OptionSchema(stringValue: Option[String]) - case class MixedOptionSchema(@JsonProperty(required=true) nonOptionValue: String, stringValue: Option[String]) + case class MixedOptionSchema(@JsonProperty(required = true) nonOptionValue: String, stringValue: Option[String]) case class WrapperOfOptionOfJsonNode(jsonNode: Option[JsonNode]) @JsonSubTypes(Array(new JsonSubTypes.Type(classOf[Impl]))) @@ -39,14 +39,13 @@ object OptionSerializerTest @JsonTypeName("impl") case class Impl() extends Base - class BaseHolder( - private var _base: Option[Base] - ) { - @(JsonTypeInfo @field)(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="$type") + class BaseHolder(private var _base: Option[Base]) { + @(JsonTypeInfo@field)(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "$type") def base = _base - def base_=(base:Option[Base]) { _base = base } + def base_=(base: Option[Base]) { + _base = base + } } - } @RunWith(classOf[JUnitRunner]) @@ -91,13 +90,14 @@ class OptionSerializerTest extends SerializerTest { } it should "honor JsonInclude.Include.NON_NULL" in { - val nonNullMapper = mapper - nonNullMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) - nonNullMapper.writeValueAsString(new NonNullOption()) should be ("{}") + // See https://github.com/FasterXML/jackson-datatype-jdk8/issues/1 for more information. + newMapper + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .writeValueAsString(new NonNullOption()) should be ("""{"foo":null}""") } it should "generate correct schema for options" in { - val schema = mapper.generateJsonSchema(classOf[OptionSchema]) + val schema = newMapper.generateJsonSchema(classOf[OptionSchema]) val schemaNode = schema.getSchemaNode val typeNode = schemaNode.path("type") @@ -117,7 +117,7 @@ class OptionSerializerTest extends SerializerTest { it should "generate correct schema for options using the new jsonSchema jackson module" in { val visitor = new SchemaFactoryWrapper() - mapper.acceptJsonFormatVisitor(mapper.constructType(classOf[OptionSchema]), visitor) + newMapper.acceptJsonFormatVisitor(newMapper.constructType(classOf[OptionSchema]), visitor) val schema = visitor.finalSchema schema should be an 'objectSchema @@ -129,7 +129,7 @@ class OptionSerializerTest extends SerializerTest { it should "mark as required the non-Option fields" in { val visitor = new SchemaFactoryWrapper() - mapper.acceptJsonFormatVisitor(mapper.constructType(classOf[MixedOptionSchema]), visitor) + newMapper.acceptJsonFormatVisitor(newMapper.constructType(classOf[MixedOptionSchema]), visitor) val schema = visitor.finalSchema() schema should be an 'objectSchema @@ -148,11 +148,11 @@ class OptionSerializerTest extends SerializerTest { it should "support reversing the default for required properties in schema" in { case class DefaultOptionSchema(nonOptionValue: String, stringValue: Option[String]) - val m = mapper + val m = newMapper m.registerModule(new RequiredPropertiesSchemaModule{}) val visitor = new SchemaFactoryWrapper() - m.acceptJsonFormatVisitor(mapper.constructType(classOf[DefaultOptionSchema]), visitor) + m.acceptJsonFormatVisitor(newMapper.constructType(classOf[DefaultOptionSchema]), visitor) val schema = visitor.finalSchema() schema should be an 'objectSchema @@ -170,21 +170,21 @@ class OptionSerializerTest extends SerializerTest { it should "serialize contained JsonNode correctly" in { val json: String = """{"prop":"value"}""" - val tree: JsonNode = mapper.readTree(json) + val tree: JsonNode = newMapper.readTree(json) val wrapperOfOptionOfJsonNode = WrapperOfOptionOfJsonNode(Some(tree)) - val actualJson: String = mapper.writeValueAsString(wrapperOfOptionOfJsonNode) + val actualJson: String = newMapper.writeValueAsString(wrapperOfOptionOfJsonNode) actualJson shouldBe """{"jsonNode":{"prop":"value"}}""" } it should "propagate type information" in { val json: String = """{"base":{"$type":"impl"}}""" - mapper.writeValueAsString(new BaseHolder(Some(Impl()))) shouldBe json + newMapper.writeValueAsString(new BaseHolder(Some(Impl()))) shouldBe json } it should "support default typing" in { - case class User(name: String, email:Option[String] = None) + case class User(name: String, email: Option[String] = None) val mapper = new ObjectMapper with ScalaObjectMapper mapper.registerModule(DefaultScalaModule) mapper.enableDefaultTyping() @@ -192,10 +192,6 @@ class OptionSerializerTest extends SerializerTest { } it should "serialize JsonTypeInfo info in Option[Seq[T]]" in { - - val mapper = new ObjectMapper - mapper.registerModule(DefaultScalaModule) - val apple = Apple("green") val basket = OrderedFruitBasket(fruits = Some(Seq(apple))) @@ -204,10 +200,6 @@ class OptionSerializerTest extends SerializerTest { } it should "serialize JsonTypeInfo info in Option[Set[T]]" in { - - val mapper = new ObjectMapper - mapper.registerModule(DefaultScalaModule) - val apple = Apple("green") val basket = NonOrderedFruitBasket(fruits = Some(Set(apple))) @@ -216,10 +208,6 @@ class OptionSerializerTest extends SerializerTest { } it should "serialize JsonTypeInfo info in Option[java.util.List[T]]" in { - - val mapper = new ObjectMapper - mapper.registerModule(DefaultScalaModule) - val apple = Apple("green") val javaFruits = new util.ArrayList[Fruit]() @@ -229,8 +217,29 @@ class OptionSerializerTest extends SerializerTest { serialize(basket) should be ("""{"fruits":[{"type":"Apple","color":"green"}]}""") } + it should "serialize with content inclusion ALWAYS" in { + val mapper = newMapper + .setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.ALWAYS)) + serialize(OptionGeneric(Option("green")), mapper) should be ("""{"data":"green"}""") + } + it should "serialize with content inclusion NON_NULL" in { + val mapper = newMapper + .setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.NON_NULL)) + serialize(OptionGeneric(Option("green")), mapper) should be ("""{"data":"green"}""") + } + it should "serialize with content inclusion NON_ABSENT" in { + val mapper = newMapper + .setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.NON_ABSENT)) + serialize(OptionGeneric(Option("green")), mapper) should be ("""{"data":"green"}""") + } + + it should "serialize with content inclusion NON_EMPTY" in { + val mapper = newMapper + .setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.NON_EMPTY)) + serialize(OptionGeneric(Option("green")), mapper) should be ("""{"data":"green"}""") + } } class NonNullOption { diff --git a/src/test/scala/com/fasterxml/jackson/module/scala/ser/SerializerTest.scala b/src/test/scala/com/fasterxml/jackson/module/scala/ser/SerializerTest.scala index 0baa8a40d..ef5348e18 100644 --- a/src/test/scala/com/fasterxml/jackson/module/scala/ser/SerializerTest.scala +++ b/src/test/scala/com/fasterxml/jackson/module/scala/ser/SerializerTest.scala @@ -1,6 +1,6 @@ package com.fasterxml.jackson.module.scala.ser -import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.fasterxml.jackson.module.scala.JacksonTest /** @@ -8,8 +8,7 @@ import com.fasterxml.jackson.module.scala.JacksonTest */ trait SerializerTest extends JacksonTest { + def serialize(value: Any, mapper: ObjectMapper = newMapper): String = mapper.writeValueAsString(value) - def serialize(value: Any): String = mapper.writeValueAsString(value) - - def jsonOf(s: String): JsonNode = mapper.readTree(s) + def jsonOf(s: String): JsonNode = newMapper.readTree(s) } \ No newline at end of file diff --git a/version.sbt b/version.sbt index 986ae9902..b723bf3c9 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "2.7.3-SNAPSHOT" \ No newline at end of file +version in ThisBuild := "2.7.4-SNAPSHOT" \ No newline at end of file