diff --git a/examples/src/main/scala/io/circe/examples/WithoutDefaults.scala b/examples/src/main/scala/io/circe/examples/WithoutDefaults.scala new file mode 100644 index 00000000..136b594d --- /dev/null +++ b/examples/src/main/scala/io/circe/examples/WithoutDefaults.scala @@ -0,0 +1,18 @@ +package io.circe.examples + +import cats.kernel.Eq +import org.scalacheck.Arbitrary + +case class WithoutDefaults(i: Int, j: Int = 1, k: Option[Int] = Some(5)) + +object WithoutDefaults { + implicit val arbitraryWithoutDefaults: Arbitrary[WithoutDefaults] = Arbitrary( + for { + i <- Arbitrary.arbitrary[Int] + j <- Arbitrary.arbitrary[Int] + k <- Arbitrary.arbitrary[Option[Int]] + } yield WithoutDefaults(i, j, k) + ) + + implicit val eqWithoutDefaults: Eq[WithoutDefaults] = Eq.fromUniversalEquals +} diff --git a/modules/derivation/shared/src/main/scala/io/circe/derivation/DerivationMacros.scala b/modules/derivation/shared/src/main/scala/io/circe/derivation/DerivationMacros.scala index 3b52ceeb..ce1436f4 100644 --- a/modules/derivation/shared/src/main/scala/io/circe/derivation/DerivationMacros.scala +++ b/modules/derivation/shared/src/main/scala/io/circe/derivation/DerivationMacros.scala @@ -839,10 +839,14 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { { val field = c.downField($realFieldName) - ${repr.decoder(member.tpe).name}.tryDecode(field) match { - case r @ _root_.scala.Right(_) if !_root_.io.circe.derivation.DerivationMacros.isKeyMissingNone(r) => r - case l @ _root_.scala.Left(_) if field.succeeded && !field.focus.exists(_.isNull) => l - case _ => _root_.scala.Right($defaultValue) + if ($useDefaults) { + ${repr.decoder(member.tpe).name}.tryDecode(field) match { + case r @ _root_.scala.Right(_) if !_root_.io.circe.derivation.DerivationMacros.isKeyMissingNone(r) => r + case l @ _root_.scala.Left(_) if field.succeeded && !field.focus.exists(_.isNull) => l + case _ => _root_.scala.Right($defaultValue) + } + } else { + ${repr.decoder(member.tpe).name}.tryDecode(field) } } """ @@ -865,11 +869,15 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { { val field = c.downField($realFieldName) - ${repr.decoder(member.tpe).name}.tryDecodeAccumulating(field) match { - case v @ _root_.cats.data.Validated.Valid(_) - if !_root_.io.circe.derivation.DerivationMacros.isKeyMissingNoneAccumulating(v) => v - case i @ _root_.cats.data.Validated.Invalid(_) if field.succeeded && !field.focus.exists(_.isNull) => i - case _ => _root_.cats.data.Validated.Valid($defaultValue) + if ($useDefaults) { + ${repr.decoder(member.tpe).name}.tryDecodeAccumulating(field) match { + case v @ _root_.cats.data.Validated.Valid(_) + if !_root_.io.circe.derivation.DerivationMacros.isKeyMissingNoneAccumulating(v) => v + case i @ _root_.cats.data.Validated.Invalid(_) if field.succeeded && !field.focus.exists(_.isNull) => i + case _ => _root_.cats.data.Validated.Valid($defaultValue) + } + } else { + ${repr.decoder(member.tpe).name}.tryDecodeAccumulating(field) } } """ diff --git a/modules/derivation/shared/src/test/scala/io/circe/derivation/DerivationSuite.scala b/modules/derivation/shared/src/test/scala/io/circe/derivation/DerivationSuite.scala index 7dd3c8c4..1003ccb1 100644 --- a/modules/derivation/shared/src/test/scala/io/circe/derivation/DerivationSuite.scala +++ b/modules/derivation/shared/src/test/scala/io/circe/derivation/DerivationSuite.scala @@ -47,6 +47,10 @@ object DerivationSuiteCodecs extends Serializable { implicit val encodeWithDefaults: Encoder[WithDefaults] = deriveEncoder(identity, None) val codecForWithDefaults: Codec[WithDefaults] = deriveCodec(identity, true, None) + implicit val decodeWithoutDefaults: Decoder[WithoutDefaults] = deriveDecoder(identity, false, None) + implicit val encodeWithoutDefaults: Encoder[WithoutDefaults] = deriveEncoder(identity, None) + val codecForWithoutDefaults: Codec[WithoutDefaults] = deriveCodec(identity, false, None) + implicit val decodeWithJson: Decoder[WithJson] = deriveDecoder(identity, true, None) implicit val encodeWithJson: Encoder[WithJson] = deriveEncoder(identity, None) val codecForWithJson: Codec[WithJson] = deriveCodec(identity, true, None) @@ -360,6 +364,16 @@ class DerivationSuite extends CirceSuite { ).codecAgreement ) + checkAll( + "CodecAgreementWithCodec[WithoutDefaults]", + CodecAgreementTests[WithoutDefaults]( + codecForWithoutDefaults, + codecForWithoutDefaults, + decodeWithoutDefaults, + encodeWithoutDefaults + ).codecAgreement + ) + checkAll( "CodecAgreementWithCodec[WithJson]", CodecAgreementTests[WithJson]( @@ -390,7 +404,7 @@ class DerivationSuite extends CirceSuite { ).codecAgreement ) - "useDefaults" should "cause defaults to be used for missing fields" in { + "useDefaults" should "cause defaults to be used for missing fields when true" in { val expectedBothDefaults = WithDefaults(0, 1, List("")) val expectedOneDefault = WithDefaults(0, 1, Nil) @@ -413,6 +427,40 @@ class DerivationSuite extends CirceSuite { assert(codecForWithDefaults.decodeAccumulating(j3.hcursor) === Validated.validNel(expectedBothDefaults)) } + "useDefaults" should "cause defaults to be ingored for missing fields when false" in { + val expectedEmptyDefault = WithoutDefaults(0, 2, None) + val expectedNoDefaults = WithoutDefaults(0, 2, Some(2)) + + val j1 = Json.obj("i" := 0) + val j2 = Json.obj("i" := 0, "j" := 2) + val j3 = Json.obj("i" := 0, "j" := 2, "k" := Json.Null) + val j4 = Json.obj("i" := 0, "j" := 2, "k" := Some(2)) + + val histories1 = List(CursorOp.DownField("j")) + + assert(decodeWithoutDefaults.decodeJson(j1).leftMap(_.history) === Left(histories1)) + assert(codecForWithoutDefaults.decodeJson(j1).leftMap(_.history) === Left(histories1)) + assert(decodeWithoutDefaults.decodeJson(j2) == Right(expectedEmptyDefault)) + assert(codecForWithoutDefaults.decodeJson(j2) === Right(expectedEmptyDefault)) + assert(decodeWithoutDefaults.decodeJson(j3) == Right(expectedEmptyDefault)) + assert(codecForWithoutDefaults.decodeJson(j3) === Right(expectedEmptyDefault)) + assert(decodeWithoutDefaults.decodeJson(j4) === Right(expectedNoDefaults)) + assert(codecForWithoutDefaults.decodeJson(j4) === Right(expectedNoDefaults)) + + val historiesNel1 = NonEmptyList.of[List[CursorOp]]( + List(CursorOp.DownField("j")) + ) + + assert(decodeWithoutDefaults.decodeAccumulating(j1.hcursor).leftMap(_.map(_.history)) === Validated.invalid(historiesNel1)) + assert(codecForWithoutDefaults.decodeAccumulating(j1.hcursor).leftMap(_.map(_.history)) === Validated.invalid(historiesNel1)) + assert(decodeWithoutDefaults.decodeAccumulating(j2.hcursor).leftMap(_.map(_.history)) === Validated.valid(expectedEmptyDefault)) + assert(codecForWithoutDefaults.decodeAccumulating(j2.hcursor).leftMap(_.map(_.history)) === Validated.valid(expectedEmptyDefault)) + assert(decodeWithoutDefaults.decodeAccumulating(j3.hcursor).leftMap(_.map(_.history)) === Validated.valid(expectedEmptyDefault)) + assert(codecForWithoutDefaults.decodeAccumulating(j3.hcursor).leftMap(_.map(_.history)) === Validated.valid(expectedEmptyDefault)) + assert(decodeWithoutDefaults.decodeAccumulating(j4.hcursor).leftMap(_.map(_.history)) === Validated.valid(expectedNoDefaults)) + assert(codecForWithoutDefaults.decodeAccumulating(j4.hcursor).leftMap(_.map(_.history)) === Validated.valid(expectedNoDefaults)) + } + "Derived ADT decoders" should "preserve error accumulation" in { val j = Json.obj("AdtFoo" := Json.obj("s" := Json.fromInt(0))).hcursor val histories = NonEmptyList.of[List[CursorOp]](