diff --git a/.gitignore b/.gitignore index 6857f79b7..4d5aac60a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ out/ *.iml .idea .DS_Store +/.bsp diff --git a/upickle/core/src/upickle/core/Config.scala b/upickle/core/src/upickle/core/Config.scala index c612f6052..61afa1cad 100644 --- a/upickle/core/src/upickle/core/Config.scala +++ b/upickle/core/src/upickle/core/Config.scala @@ -68,6 +68,12 @@ trait Config { */ def optionsAsNulls: Boolean = true + /** + * When `optionsAsNull`s is enabled, Whether top-level `None`s are serialized as + * `null` or are omitted + */ + def serializeNones: Boolean = true + /** * Configure whether you want upickle to skip unknown keys during de-serialization * of `case class`es. Can be overriden for the entire serializer via `override def`, and diff --git a/upickle/implicits/src-2/upickle/implicits/internal/Macros2.scala b/upickle/implicits/src-2/upickle/implicits/internal/Macros2.scala index 997821446..4649fd3bd 100644 --- a/upickle/implicits/src-2/upickle/implicits/internal/Macros2.scala +++ b/upickle/implicits/src-2/upickle/implicits/internal/Macros2.scala @@ -466,6 +466,13 @@ object Macros2 { yield q"this.storeValueIfNotFound($i, ${defaultValues(i).get})" } + if (!${c.prefix}.serializeNones) { + ..${ + for(i <- types.indices if types(i).typeSymbol.fullName == "scala.Option") + yield q"this.storeValueIfNotFound($i, None)" + } + } + // Special-case 64 because java bit shifting ignores any RHS values above 63 // https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.19 if (${ @@ -564,9 +571,22 @@ object Macros2 { $select ) """ - val default = if (defaultValue.isEmpty) snippet - else q"""if (${serDfltVals(symbol)} || $select != ${defaultValue.get}) $snippet""" - default :: Nil + + val isOption = tpeOfField.typeSymbol.fullName == "scala.Option" + val hasDefault = defaultValue.nonEmpty + + def nonesCond = q"${c.prefix}.serializeNones || $select.nonEmpty" + def defaultsCond = q"${serDfltVals(symbol)} || $select != ${defaultValue.get}" + + val res = if (isOption && hasDefault) { + q"""if (($nonesCond) && ($defaultsCond)) $snippet""" + } else if(hasDefault) { + q"""if ($defaultsCond) $snippet""" + } else if(isOption) { + q"""if ($nonesCond) $snippet""" + } else snippet + + res :: Nil } } } @@ -586,9 +606,21 @@ object Macros2 { } else fail(s"Invalid type for flattening: $tpeOfField.") case None => - val snippet = if (defaultValue.isEmpty) q"1" - else q"""if (${serDfltVals(symbol)} || $select != ${defaultValue.get}) 1 else 0""" - snippet :: Nil + val isOption = tpeOfField.typeSymbol.fullName == "scala.Option" + val hasDefault = defaultValue.nonEmpty + + def nonesCond = q"${c.prefix}.serializeNones || $select.nonEmpty" + def defaultsCond = q"${serDfltVals(symbol)} || $select != ${defaultValue.get}" + + val res = if (isOption && hasDefault) { + q"""if (($nonesCond) && ($defaultsCond)) 1 else 0""" + } else if(hasDefault) { + q"""if ($defaultsCond) 1 else 0""" + } else if(isOption) { + q"""if ($nonesCond) 1 else 0""" + } else q"1" + + res :: Nil } } } diff --git a/upickle/implicits/src-3/upickle/implicits/macros.scala b/upickle/implicits/src-3/upickle/implicits/macros.scala index 801294720..1651169e9 100644 --- a/upickle/implicits/src-3/upickle/implicits/macros.scala +++ b/upickle/implicits/src-3/upickle/implicits/macros.scala @@ -117,10 +117,14 @@ private def storeDefaultsImpl[T](x: Expr[upickle.implicits.BaseCaseObjectContext val statements = allFields[T] .filter(!_._5) .zipWithIndex - .map { case ((_, _, _, default, _), i) => + .map { case ((field, _, _, default, _), i) => default match { case Some(defaultValue) => '{${x}.storeValueIfNotFound(${Expr(i)}, ${defaultValue})} - case None => '{} + case None => + field.typeRef.asType match { + case '[Option[?]] => '{${x}.storeValueIfNotFound(${Expr(i)}, None)} + case _ => '{} + } } } @@ -239,12 +243,22 @@ private def writeLengthImpl[T](thisOuter: Expr[upickle.core.Types with upickle.i report.error(s"${typeSymbol} is not a case class or a Iterable[(String, _)]") Nil } - } - else if (!defaults.contains(label)) List('{1}) - else { - val serDflt = serDfltVals(thisOuter, field, classTypeRepr.typeSymbol) + } else { + val isOption = field.typeRef.asType match { + case '[Option[?]] => true + case _ => false + } + val hasDefault = defaults.contains(label) + def defaultsCond = { + val serDflt = serDfltVals(thisOuter, field, classTypeRepr.typeSymbol) + '{ ${serDflt} || ${select.asExprOf[Any]} != ${defaults(label)} } + } + def nonesCond = '{ ${ thisOuter }.serializeNones || ${ select.asExprOf[Option[?]] }.nonEmpty } List( - '{if (${serDflt} || ${select.asExprOf[Any]} != ${defaults(label)}) 1 else 0} + if (hasDefault && isOption) '{if ($defaultsCond && $nonesCond) 1 else 0} + else if (hasDefault) '{if ($defaultsCond) 1 else 0} + else if (isOption) '{if ($nonesCond) 1 else 0} + else '{1} ) } @@ -330,12 +344,21 @@ private def writeSnippetsImpl[R, T, W[_]](thisOuter: Expr[upickle.core.Types wit ${select.asExprOf[Any]}, ) } + val isOption = field.typeRef.asType match { + case '[Option[?]] => true + case _ => false + } + val hasDefault = defaults.contains(label) + def nonesCond = '{ ${ thisOuter }.serializeNones || ${select.asExprOf[Option[?]]}.nonEmpty } + def defaultsCond = { + val serDflt = serDfltVals(thisOuter, field, classTypeRepr.typeSymbol) + '{ ${serDflt} || ${select.asExprOf[Any]} != ${defaults(label)} } + } List( - if (!defaults.contains(label)) snippet - else { - val serDflt = serDfltVals(thisOuter, field, classTypeRepr.typeSymbol) - '{if ($serDflt || ${select.asExprOf[Any]} != ${defaults(label)}) $snippet} - } + if (hasDefault && isOption) '{ if ($defaultsCond && $nonesCond) $snippet } + else if (hasDefault) '{ if ($defaultsCond) $snippet } + else if (isOption) '{ if ($nonesCond) $snippet } + else snippet ) } diff --git a/upickle/test/src/upickle/example/ExampleTests.scala b/upickle/test/src/upickle/example/ExampleTests.scala index 63f8013f7..ad63daf33 100644 --- a/upickle/test/src/upickle/example/ExampleTests.scala +++ b/upickle/test/src/upickle/example/ExampleTests.scala @@ -53,6 +53,12 @@ object Defaults{ implicit val rw: RW[FooDefault] = macroRW } } +object Nones{ + case class OptionWrapper(opt: Option[String]) + object OptionWrapper{ + implicit val rw: RW[OptionWrapper] = macroRW + } +} object Keyed{ case class KeyBar(@upickle.implicits.key("hehehe") kekeke: Int) object KeyBar{ @@ -105,6 +111,7 @@ import Sealed._ import Simple._ import Recursive._ import Defaults._ +import Nones._ object ExampleTests extends TestSuite { @@ -417,6 +424,28 @@ object ExampleTests extends TestSuite { SerializeDefaults.write(FooDefault(i = 10, s = "lol")) ==> """{"i":10,"s":"lol"}""" SerializeDefaults.write(FooDefault()) ==> """{"i":10,"s":"lol"}""" } + test("serializeNones = false"){ + object SerializeNones extends upickle.AttributeTagged{ + override def serializeNones = false + } + implicit val optionWrapperRW: SerializeNones.ReadWriter[OptionWrapper] = SerializeNones.macroRW + SerializeNones.write(OptionWrapper(None)) ==> "{}" + SerializeNones.write(OptionWrapper(opt = Some("lol"))) ==> """{"opt":"lol"}""" + + SerializeNones.read[OptionWrapper]("{}") ==> OptionWrapper(None) + SerializeNones.read[OptionWrapper]("""{"opt":"lol"}""") ==> OptionWrapper(opt = Some("lol")) + } + test("serializeNones = true"){ + object SerializeNones extends upickle.AttributeTagged{ + override def serializeNones = true + } + implicit val optionWrapperRW: SerializeNones.ReadWriter[OptionWrapper] = SerializeNones.macroRW + SerializeNones.write(OptionWrapper(None)) ==> """{"opt":null}""" + SerializeNones.write(OptionWrapper(opt = Some("lol"))) ==> """{"opt":"lol"}""" + + SerializeNones.read[OptionWrapper]("""{"opt":null}""") ==> OptionWrapper(None) + SerializeNones.read[OptionWrapper]("""{"opt":"lol"}""") ==> OptionWrapper(opt = Some("lol")) + } } test("transform"){