From fa9c2d98efa536fbe1d8781664bbf314206d3632 Mon Sep 17 00:00:00 2001 From: Samuel Nelson Date: Mon, 3 Oct 2022 18:17:01 +1300 Subject: [PATCH] Implement strict decoding --- .../annotations/Configuration.scala | 11 +- .../derivation/annotations/JsonCodec.scala | 14 +- .../circe/derivation/DerivationMacros.scala | 127 ++++++++++++++--- .../scala/io/circe/derivation/package.scala | 6 +- .../io/circe/derivation/DerivationSuite.scala | 28 ++-- .../derivation/StrictDecodingExample.scala | 55 ++++++++ .../derivation/StrictDecodingSuite.scala | 130 ++++++++++++++++++ .../TransformConstructorNamesSuite.scala | 16 +-- .../TransformMemberNamesExample.scala | 10 +- .../TransformMemberNamesSuite.scala | 17 +-- 10 files changed, 350 insertions(+), 64 deletions(-) create mode 100644 modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingExample.scala create mode 100644 modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingSuite.scala diff --git a/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/Configuration.scala b/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/Configuration.scala index 86a03f2d..f9ad7ce1 100644 --- a/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/Configuration.scala +++ b/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/Configuration.scala @@ -25,6 +25,8 @@ sealed trait Configuration { */ def transformConstructorNames: String => String + def strictDecoding: Boolean + def useDefaults: Boolean def discriminator: Option[String] @@ -67,7 +69,8 @@ object Configuration { transformMemberNames: String => String, transformConstructorNames: String => String, useDefaults: Boolean, - discriminator: Option[String] + discriminator: Option[String], + strictDecoding: Boolean = false ) extends Configuration { type Config = Codec @@ -94,7 +97,8 @@ object Configuration { transformMemberNames: String => String, transformConstructorNames: String => String, useDefaults: Boolean, - discriminator: Option[String] + discriminator: Option[String], + strictDecoding: Boolean = false ) extends Configuration { type Config = DecodeOnly @@ -119,7 +123,8 @@ object Configuration { transformMemberNames: String => String, transformConstructorNames: String => String, useDefaults: Boolean, - discriminator: Option[String] + discriminator: Option[String], + strictDecoding: Boolean = false ) extends Configuration { type Config = EncodeOnly diff --git a/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/JsonCodec.scala b/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/JsonCodec.scala index 39c591e7..81c38393 100644 --- a/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/JsonCodec.scala +++ b/modules/annotations/shared/src/main/scala/io/circe/derivation/annotations/JsonCodec.scala @@ -65,11 +65,11 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context) val Type = tq"$objName.type" val (decoder, encoder, codec) = ( q"""implicit val $decoderName: $DecoderClass[$Type] = - _root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""", + _root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""", q"""implicit val $encoderName: $AsObjectEncoderClass[$Type] = _root_.io.circe.derivation.deriveEncoder[$Type]($transformNames, $cfgDiscriminator)""", q"""implicit val $codecName: $AsObjectCodecClass[$Type] = - _root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""" + _root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""" ) codecType match { case JsonCodecType.Both => codec @@ -141,6 +141,8 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context) q"$config.useDefaults" private[this] val cfgDiscriminator = q"$config.discriminator" + private[this] val cfgStrictDecoding = + q"$config.strictDecoding" private[this] def codec(clsDef: ClassDef): Tree = { val tpname = clsDef.name @@ -153,11 +155,11 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context) val Type = tpname ( q"""implicit val $decoderName: $DecoderClass[$Type] = - _root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""", + _root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""", q"""implicit val $encoderName: $AsObjectEncoderClass[$Type] = _root_.io.circe.derivation.deriveEncoder[$Type]($transformNames, $cfgDiscriminator)""", q"""implicit val $codecName: $AsObjectCodecClass[$Type] = - _root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""" + _root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""" ) } else { val tparamNames = tparams.map(_.name) @@ -172,13 +174,13 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context) val Type = tq"$tpname[..$tparamNames]" ( q"""implicit def $decoderName[..$tparams](implicit ..$decodeParams): $DecoderClass[$Type] = - _root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""", + _root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""", q"""implicit def $encoderName[..$tparams](implicit ..$encodeParams): $AsObjectEncoderClass[$Type] = _root_.io.circe.derivation.deriveEncoder[$Type]($transformNames, $cfgDiscriminator)""", q"""implicit def $codecName[..$tparams](implicit ..${decodeParams ++ encodeParams} ): $AsObjectCodecClass[$Type] = - _root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""" + _root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""" ) } codecType match { 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..170bc9fc 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 @@ -15,6 +15,7 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { private[this] val defaultDiscriminator = c.Expr[Option[String]](q"_root_.scala.None") private[this] val trueExpression = c.Expr[Boolean](q"true") + private[this] val falseExpression = c.Expr[Boolean](q"false") private[this] def failWithMessage(message: String): Nothing = c.abort(c.enclosingPosition, message) private[this] def nameOf(s: Symbol): String = s.asClass.name.decodedName.toString.trim @@ -248,23 +249,25 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { } def materializeDecoder[T: c.WeakTypeTag]: c.Expr[Decoder[T]] = - materializeDecoderImpl[T](None, trueExpression, defaultDiscriminator) + materializeDecoderImpl[T](None, trueExpression, defaultDiscriminator, falseExpression) def materializeEncoder[T: c.WeakTypeTag]: c.Expr[Encoder.AsObject[T]] = materializeEncoderImpl[T](None, defaultDiscriminator) def materializeCodec[T: c.WeakTypeTag]: c.Expr[Codec.AsObject[T]] = - materializeCodecImpl[T](None, trueExpression, defaultDiscriminator) + materializeCodecImpl[T](None, trueExpression, defaultDiscriminator, falseExpression) def materializeDecoderWithTransformNames[T: c.WeakTypeTag]( transformNames: c.Expr[String => String], useDefaults: c.Expr[Boolean], - discriminator: c.Expr[Option[String]] + discriminator: c.Expr[Option[String]], + strictDecoding: c.Expr[Boolean] ): c.Expr[Decoder[T]] = materializeDecoderImpl[T]( Some(transformNames), useDefaults, - discriminator + discriminator, + strictDecoding ) def materializeDecoderWithTransformNamesAndDefaults[T: c.WeakTypeTag]( @@ -273,7 +276,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { materializeDecoderImpl[T]( Some(transformNames), trueExpression, - defaultDiscriminator + defaultDiscriminator, + falseExpression ) def materializeEncoderWithTransformNames[T: c.WeakTypeTag]( @@ -290,12 +294,14 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { def materializeCodecWithTransformNames[T: c.WeakTypeTag]( transformNames: c.Expr[String => String], useDefaults: c.Expr[Boolean], - discriminator: c.Expr[Option[String]] + discriminator: c.Expr[Option[String]], + strictDecoding: c.Expr[Boolean] ): c.Expr[Codec.AsObject[T]] = materializeCodecImpl[T]( Some(transformNames), useDefaults, - discriminator + discriminator, + strictDecoding ) def materializeCodecWithTransformNamesAndDefaults[T: c.WeakTypeTag]( @@ -304,13 +310,15 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { materializeCodecImpl[T]( Some(transformNames), trueExpression, - defaultDiscriminator + defaultDiscriminator, + falseExpression ) private[this] def materializeCodecImpl[T: c.WeakTypeTag]( transformNames: Option[c.Expr[String => String]], useDefaults: c.Expr[Boolean], - discriminator: c.Expr[Option[String]] + discriminator: c.Expr[Option[String]], + strictDecoding: c.Expr[Boolean] ): c.Expr[Codec.AsObject[T]] = { val tpe = weakTypeOf[T] @@ -319,7 +327,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { materializeCodecCaseClassImpl[T]( transformNames, useDefaults, - discriminator + discriminator, + strictDecoding ) } else { materializeCodecTraitImpl[T]( @@ -334,18 +343,20 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { private[this] def materializeDecoderImpl[T: c.WeakTypeTag]( transformNames: Option[c.Expr[String => String]], useDefaults: c.Expr[Boolean], - discriminator: c.Expr[Option[String]] + discriminator: c.Expr[Option[String]], + strictDecoding: c.Expr[Boolean] ): c.Expr[Decoder[T]] = { val tpe = weakTypeOf[T] val subclasses = allSubclasses(tpe.typeSymbol.asClass) if (subclasses.isEmpty) { - materializeDecoderCaseClassImpl[T](transformNames, useDefaults) + materializeDecoderCaseClassImpl[T](transformNames, useDefaults, strictDecoding) } else { materializeDecoderTraitImpl[T]( transformNames, subclasses, - discriminator + discriminator, + strictDecoding ) } } @@ -353,7 +364,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { private[this] def materializeDecoderTraitImpl[T: c.WeakTypeTag]( transformConstructorNames: Option[c.Expr[String => String]], subclasses: Set[Symbol], - discriminator: c.Expr[Option[String]] + discriminator: c.Expr[Option[String]], + strictDecoding: c.Expr[Boolean] ): c.Expr[Decoder[T]] = { val tpe = weakTypeOf[T] @@ -420,7 +432,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { private[this] def materializeDecoderCaseClassImpl[T: c.WeakTypeTag]( transformMemberNames: Option[c.Expr[String => String]], - useDefaults: c.Expr[Boolean] + useDefaults: c.Expr[Boolean], + strictDecoding: c.Expr[Boolean] ): c.Expr[Decoder[T]] = { val tpe = weakTypeOf[T] @@ -527,7 +540,7 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { } """ - c.Expr[Decoder[T]]( + val nonStrictDecoder = c.Expr[Decoder[T]]( q""" new _root_.io.circe.Decoder[$tpe] { ..$instanceDefs @@ -547,6 +560,40 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { } """ ) + + val expectedFields: Tree = repr.paramListsWithNames.flatten match { + case Nil => q"""_root_.scala.Predef.Set.empty[String]""" + case head :: tail => + tail.foldLeft(q"""_root_.scala.Predef.Set(${head._1.keyName.getOrElse(transformName(head._1.decodedName))})""") { + case (acc, (member, _)) => + q"""$acc ++ _root_.scala.Predef.Set(${member.keyName.getOrElse(transformName(member.decodedName))})""" + } + } + + c.Expr[Decoder[T]]( + q""" + val nonStrictDecoder = $nonStrictDecoder + + if ($strictDecoding) { + val expectedFields = $expectedFields + nonStrictDecoder.validate { cursor: _root_.io.circe.HCursor => + val maybeUnexpectedErrors = for { + json <- cursor.focus + jsonKeys <- json.hcursor.keys + unexpected = jsonKeys.toSet -- expectedFields + } yield { + unexpected.toList.map { unexpectedField => + s"Unexpected field: [$$unexpectedField]; valid fields: $${expectedFields.mkString(", ")}" + } + } + + maybeUnexpectedErrors.getOrElse(List("Couldn't determine decoded fields.")) + } + } else { + nonStrictDecoder + } + """ + ) } } } @@ -880,7 +927,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { private[this] def materializeCodecCaseClassImpl[T: c.WeakTypeTag]( transformMemberNames: Option[c.Expr[String => String]], useDefaults: c.Expr[Boolean], - discriminator: c.Expr[Option[String]] + discriminator: c.Expr[Option[String]], + strictDecoding: c.Expr[Boolean] ): c.Expr[Codec.AsObject[T]] = { val tpe = weakTypeOf[T] @@ -1030,7 +1078,7 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { } """ - c.Expr[Codec.AsObject[T]]( + val nonStrictCodec = c.Expr[Codec.AsObject[T]]( q""" new _root_.io.circe.Codec.AsObject[$tpe] { ..$encoderInstanceDefs @@ -1054,6 +1102,49 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat { } """ ) + + val expectedFields: Tree = repr.paramListsWithNames.flatten match { + case Nil => q"""_root_.scala.Predef.Set.empty[String]""" + case head :: tail => + tail.foldLeft(q"""_root_.scala.Predef.Set(${head._1.keyName.getOrElse(transformName(head._1.decodedName))})""") { + case (acc, (member, _)) => + q"""$acc ++ _root_.scala.Predef.Set(${member.keyName.getOrElse(transformName(member.decodedName))})""" + } + } + + c.Expr[Codec.AsObject[T]]( + q""" + val nonStrictCodec = $nonStrictCodec + + if ($strictDecoding) { + val expectedFields = $expectedFields + val strictDecoder = nonStrictCodec.validate { cursor: _root_.io.circe.HCursor => + val maybeUnexpectedErrors = for { + json <- cursor.focus + jsonKeys <- json.hcursor.keys + unexpected = jsonKeys.toSet -- expectedFields + } yield { + unexpected.toList.map { unexpectedField => + s"Unexpected field: [$$unexpectedField]; valid fields: $${expectedFields.mkString(", ")}" + } + } + + maybeUnexpectedErrors.getOrElse(List("Couldn't determine decoded fields.")) + } + new _root_.io.circe.Codec.AsObject[$tpe] { + final def encodeObject(a: $tpe): _root_.io.circe.JsonObject = nonStrictCodec.encodeObject(a) + + final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[$tpe] = strictDecoder(c) + + final override def decodeAccumulating( + c: _root_.io.circe.HCursor + ): _root_.io.circe.Decoder.AccumulatingResult[$tpe] = strictDecoder.decodeAccumulating(c) + } + } else { + nonStrictCodec + } + """ + ) } } } diff --git a/modules/derivation/shared/src/main/scala/io/circe/derivation/package.scala b/modules/derivation/shared/src/main/scala/io/circe/derivation/package.scala index e0369935..a8d314c8 100644 --- a/modules/derivation/shared/src/main/scala/io/circe/derivation/package.scala +++ b/modules/derivation/shared/src/main/scala/io/circe/derivation/package.scala @@ -10,7 +10,8 @@ package object derivation { final def deriveDecoder[A]( transformNames: String => String, useDefaults: Boolean, - discriminator: Option[String] + discriminator: Option[String], + strictDecoding: Boolean ): Decoder[A] = macro DerivationMacros.materializeDecoderWithTransformNames[A] @@ -29,7 +30,8 @@ package object derivation { final def deriveCodec[A]( transformNames: String => String, useDefaults: Boolean, - discriminator: Option[String] + discriminator: Option[String], + strictDecoding: Boolean ): Codec.AsObject[A] = macro DerivationMacros.materializeCodecWithTransformNames[A] 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..6d4e2e50 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 @@ -43,13 +43,13 @@ object DerivationSuiteCodecs extends Serializable { implicit val encodeCustomApplyParamTypesClass: Encoder[CustomApplyParamTypesClass] = deriveEncoder val codecForCustomApplyParamTypesClass: Codec[CustomApplyParamTypesClass] = deriveCodec - implicit val decodeWithDefaults: Decoder[WithDefaults] = deriveDecoder(identity, true, None) + implicit val decodeWithDefaults: Decoder[WithDefaults] = deriveDecoder(identity, true, None, false) implicit val encodeWithDefaults: Encoder[WithDefaults] = deriveEncoder(identity, None) - val codecForWithDefaults: Codec[WithDefaults] = deriveCodec(identity, true, None) + val codecForWithDefaults: Codec[WithDefaults] = deriveCodec(identity, true, None, false) - implicit val decodeWithJson: Decoder[WithJson] = deriveDecoder(identity, true, None) + implicit val decodeWithJson: Decoder[WithJson] = deriveDecoder(identity, true, None, false) implicit val encodeWithJson: Encoder[WithJson] = deriveEncoder(identity, None) - val codecForWithJson: Codec[WithJson] = deriveCodec(identity, true, None) + val codecForWithJson: Codec[WithJson] = deriveCodec(identity, true, None, false) implicit val decodeAdtFoo: Decoder[AdtFoo] = deriveDecoder implicit val encodeAdtFoo: Encoder.AsObject[AdtFoo] = deriveEncoder @@ -79,31 +79,31 @@ object DerivationSuiteCodecs extends Serializable { object discriminator { val typeField = Some("_type") - implicit val decodeAdtFoo: Decoder[AdtFoo] = deriveDecoder(identity, false, typeField) + implicit val decodeAdtFoo: Decoder[AdtFoo] = deriveDecoder(identity, false, typeField, false) implicit val encodeAdtFoo: Encoder.AsObject[AdtFoo] = deriveEncoder(identity, typeField) - implicit val decodeAdtBar: Decoder[AdtBar] = deriveDecoder(identity, false, typeField) + implicit val decodeAdtBar: Decoder[AdtBar] = deriveDecoder(identity, false, typeField, false) implicit val encodeAdtBar: Encoder.AsObject[AdtBar] = deriveEncoder(identity, typeField) - implicit val decodeAdtQux: Decoder[AdtQux.type] = deriveDecoder(identity, false, typeField) + implicit val decodeAdtQux: Decoder[AdtQux.type] = deriveDecoder(identity, false, typeField, false) implicit val encodeAdtQux: Encoder.AsObject[AdtQux.type] = deriveEncoder(identity, typeField) - implicit val decodeAdt: Decoder[Adt] = deriveDecoder(identity, false, typeField) + implicit val decodeAdt: Decoder[Adt] = deriveDecoder(identity, false, typeField, false) implicit val encodeAdt: Encoder.AsObject[Adt] = deriveEncoder(identity, typeField) - val codecForAdt: Codec[Adt] = deriveCodec(identity, false, typeField) + val codecForAdt: Codec[Adt] = deriveCodec(identity, false, typeField, false) - implicit val decodeNestedAdtFoo: Decoder[NestedAdtFoo] = deriveDecoder(identity, false, typeField) + implicit val decodeNestedAdtFoo: Decoder[NestedAdtFoo] = deriveDecoder(identity, false, typeField, false) implicit val encodeNestedAdtFoo: Encoder.AsObject[NestedAdtFoo] = deriveEncoder(identity, typeField) - implicit val decodeNestedAdtBar: Decoder[NestedAdtBar] = deriveDecoder(identity, false, typeField) + implicit val decodeNestedAdtBar: Decoder[NestedAdtBar] = deriveDecoder(identity, false, typeField, false) implicit val encodeNestedAdtBar: Encoder.AsObject[NestedAdtBar] = deriveEncoder(identity, typeField) - implicit val decodeNestedAdtQux: Decoder[NestedAdtQux.type] = deriveDecoder(identity, false, typeField) + implicit val decodeNestedAdtQux: Decoder[NestedAdtQux.type] = deriveDecoder(identity, false, typeField, false) implicit val encodeNestedAdtQux: Encoder.AsObject[NestedAdtQux.type] = deriveEncoder(identity, typeField) - implicit val decodeNestedAdt: Decoder[NestedAdt] = deriveDecoder(identity, false, typeField) + implicit val decodeNestedAdt: Decoder[NestedAdt] = deriveDecoder(identity, false, typeField, false) implicit val encodeNestedAdt: Encoder.AsObject[NestedAdt] = deriveEncoder(identity, typeField) - val codecForNestedAdt: Codec[NestedAdt] = deriveCodec(identity, false, typeField) + val codecForNestedAdt: Codec[NestedAdt] = deriveCodec(identity, false, typeField, false) } } diff --git a/modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingExample.scala b/modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingExample.scala new file mode 100644 index 00000000..7bafebf6 --- /dev/null +++ b/modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingExample.scala @@ -0,0 +1,55 @@ +package io.circe.derivation + +import cats.kernel.Eq +import io.circe.{Codec, Decoder, Encoder} +import org.scalacheck.Arbitrary + +object StrictDecodingExample { + case class User(firstName: String, lastName: String, role: Role, address: Address) + + object User { + implicit val arbitraryUser: Arbitrary[User] = Arbitrary( + for { + f <- Arbitrary.arbitrary[String] + l <- Arbitrary.arbitrary[String] + r <- Arbitrary.arbitrary[Role] + a <- Arbitrary.arbitrary[Address] + } yield User(f, l, r, a) + ) + + implicit val eqUser: Eq[User] = Eq.fromUniversalEquals + + implicit val encodeUser: Encoder[User] = deriveEncoder(renaming.snakeCase, None) + implicit val decodeUser: Decoder[User] = deriveDecoder(renaming.snakeCase, true, None, true) + val codecForUser: Codec[User] = deriveCodec(renaming.snakeCase, true, None, true) + } + + case class Role(title: String) + + object Role { + implicit val arbitraryRole: Arbitrary[Role] = Arbitrary(Arbitrary.arbitrary[String].map(Role(_))) + implicit val eqRole: Eq[Role] = Eq.fromUniversalEquals + + implicit val encodeRole: Encoder[Role] = deriveEncoder(_.toUpperCase, None) + implicit val decodeRole: Decoder[Role] = deriveDecoder(_.toUpperCase, true, None, true) + } + + case class Address(number: Int, street: String, city: String) + + object Address { + implicit val arbitraryAddress: Arbitrary[Address] = Arbitrary( + for { + n <- Arbitrary.arbitrary[Int] + c <- Arbitrary.arbitrary[String] + s <- Arbitrary.arbitrary[String] + } yield Address(n, c, s) + ) + + implicit val eqAddress: Eq[Address] = Eq.fromUniversalEquals + + implicit val encodeAddress: Encoder[Address] = + deriveEncoder(renaming.replaceWith("number" -> "#"), None) + implicit val decodeAddress: Decoder[Address] = + deriveDecoder(renaming.replaceWith("number" -> "#"), true, None, true) + } +} diff --git a/modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingSuite.scala b/modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingSuite.scala new file mode 100644 index 00000000..5b3455e9 --- /dev/null +++ b/modules/derivation/shared/src/test/scala/io/circe/derivation/StrictDecodingSuite.scala @@ -0,0 +1,130 @@ +package io.circe.derivation + +import cats.data.{NonEmptyList, Validated} +import io.circe.examples.{Bar, Baz, Foo, Qux} +import io.circe.{Codec, CursorOp, Decoder, DecodingFailure, Encoder, Json} +import io.circe.testing.CodecTests + +object StrictDecodingSuiteCodecs extends Serializable { + implicit val decodeFoo: Decoder[Foo] = deriveDecoder(renaming.snakeCase, true, None, true) + implicit val encodeFoo: Encoder.AsObject[Foo] = deriveEncoder(renaming.snakeCase, None) + val codecForFoo: Codec.AsObject[Foo] = deriveCodec(renaming.snakeCase, true, None, true) + + implicit val decodeBar: Decoder[Bar] = deriveDecoder(renaming.snakeCase, true, None, true) + implicit val encodeBar: Encoder.AsObject[Bar] = deriveEncoder(renaming.snakeCase, None) + val codecForBar: Codec.AsObject[Bar] = deriveCodec(renaming.snakeCase, true, None, true) + + implicit val decodeBaz: Decoder[Baz] = deriveDecoder(renaming.snakeCase, true, None, true) + implicit val encodeBaz: Encoder.AsObject[Baz] = deriveEncoder(renaming.snakeCase, None) + val codecForBaz: Codec.AsObject[Baz] = deriveCodec(renaming.snakeCase, true, None, true) + + implicit def decodeQux[A: Decoder]: Decoder[Qux[A]] = + deriveDecoder(renaming.replaceWith("aa" -> "1", "bb" -> "2"), true, None, true) + implicit def encodeQux[A: Encoder]: Encoder.AsObject[Qux[A]] = + deriveEncoder(renaming.replaceWith("aa" -> "1", "bb" -> "2"), None) + def codecForQux[A: Decoder: Encoder]: Codec.AsObject[Qux[A]] = deriveCodec( + renaming.replaceWith("aa" -> "1", "bb" -> "2"), + true, + None, + true + ) +} + +class StrictDecodingSuite extends CirceSuite { + import StrictDecodingExample._ + import StrictDecodingSuiteCodecs._ + + checkAll("Codec[Foo]", CodecTests[Foo].codec) + checkAll("Codec[Foo] via Codec", CodecTests[Foo](codecForFoo, codecForFoo).codec) + checkAll("Codec[Bar]", CodecTests[Bar].codec) + checkAll("Codec[Bar] via Codec", CodecTests[Bar](codecForBar, codecForBar).codec) + checkAll("Codec[Baz]", CodecTests[Baz].codec) + checkAll("Codec[Baz] via Codec", CodecTests[Baz](codecForBaz, codecForBaz).codec) + checkAll("Codec[Qux[Baz]]", CodecTests[Qux[Baz]].codec) + checkAll("Codec[Qux[Baz]] via Codec", CodecTests[Qux[Baz]](codecForQux, codecForQux).codec) + + checkAll("Codec[User]", CodecTests[User].codec) + checkAll("Codec[User] via Codec", CodecTests[User](User.codecForUser, User.codecForUser).codec) + + checkAll( + "CodecAgreementWithCodec[Foo]", + CodecAgreementTests[Foo]( + codecForFoo, + codecForFoo, + decodeFoo, + encodeFoo + ).codecAgreement + ) + + checkAll( + "CodecAgreementWithCodec[Bar]", + CodecAgreementTests[Bar]( + codecForBar, + codecForBar, + decodeBar, + encodeBar + ).codecAgreement + ) + + checkAll( + "CodecAgreementWithCodec[Baz]", + CodecAgreementTests[Baz]( + codecForBaz, + codecForBaz, + decodeBaz, + encodeBaz + ).codecAgreement + ) + + checkAll( + "CodecAgreementWithCodec[Qux[Baz]]", + CodecAgreementTests[Qux[Baz]]( + codecForQux, + codecForQux, + decodeQux, + encodeQux + ).codecAgreement + ) + + "deriveDecoder" should "return error when json has extra fields" in { + val j1 = Json.obj( + "first_name" -> Json.fromString("John"), + "last_name" -> Json.fromString("Smith"), + "role" -> Json.obj("TITLE" -> Json.fromString("Entrepreneur")), + "address" -> Json.obj( + "#" -> Json.fromInt(5), + "street" -> Json.fromString("Elm Street"), + "city" -> Json.fromString("Springfield"), + "foo" -> Json.fromString("bar") + ) + ) + val j2 = Json.obj( + "unexpected1" -> Json.fromInt(1), + "unexpected2" -> Json.fromInt(2), + "first_name" -> Json.fromString("John"), + "last_name" -> Json.fromString("Smith") + ) + + val expectedFailure1 = DecodingFailure("Unexpected field: [foo]; valid fields: #, street, city", List(CursorOp.DownField("address"))) + val expectedFailure2 = DecodingFailure( + "Unexpected field: [unexpected1]; valid fields: first_name, last_name, role, address", + List.empty + ) + + assert(j1.as[User] === Left(expectedFailure1)) + assert(j2.as[User] === Left(expectedFailure2)) + + val expectedFailureNel1 = NonEmptyList.of( + expectedFailure1, + ) + val expectedFailureNel2 = NonEmptyList.of( + expectedFailure2, + DecodingFailure("Unexpected field: [unexpected2]; valid fields: first_name, last_name, role, address", List.empty), + DecodingFailure("Attempt to decode value on failed cursor", List(CursorOp.DownField("role"))), + DecodingFailure("Attempt to decode value on failed cursor", List(CursorOp.DownField("address"))) + ) + + assert(User.decodeUser.decodeAccumulating(j1.hcursor) === Validated.invalid(expectedFailureNel1)) + assert(User.decodeUser.decodeAccumulating(j2.hcursor) === Validated.invalid(expectedFailureNel2)) + } +} diff --git a/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformConstructorNamesSuite.scala b/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformConstructorNamesSuite.scala index 27ba1da0..b84b4707 100644 --- a/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformConstructorNamesSuite.scala +++ b/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformConstructorNamesSuite.scala @@ -16,9 +16,9 @@ object TransformConstructorNamesSuite extends Serializable { implicit val decodeAdtQux: Decoder[AdtQux.type] = deriveDecoder implicit val encodeAdtQux: Encoder.AsObject[AdtQux.type] = deriveEncoder - implicit val decodeAdt: Decoder[Adt] = deriveDecoder(renaming.snakeCase, true, None) + implicit val decodeAdt: Decoder[Adt] = deriveDecoder(renaming.snakeCase, true, None, false) implicit val encodeAdt: Encoder.AsObject[Adt] = deriveEncoder(renaming.snakeCase, None) - val codecForAdt: Codec[Adt] = deriveCodec(renaming.snakeCase, true, None) + val codecForAdt: Codec[Adt] = deriveCodec(renaming.snakeCase, true, None, false) implicit val decodeNestedAdtBar: Decoder[NestedAdtBar] = deriveDecoder implicit val encodeNestedAdtBar: Encoder.AsObject[NestedAdtBar] = deriveEncoder @@ -29,20 +29,20 @@ object TransformConstructorNamesSuite extends Serializable { implicit val decodeNestedAdtQux: Decoder[NestedAdtQux.type] = deriveDecoder implicit val encodeNestedAdtQux: Encoder.AsObject[NestedAdtQux.type] = deriveEncoder - implicit val decodeNestedAdt: Decoder[NestedAdt] = deriveDecoder(renaming.snakeCase, true, None) + implicit val decodeNestedAdt: Decoder[NestedAdt] = deriveDecoder(renaming.snakeCase, true, None, false) implicit val encodeNestedAdt: Encoder.AsObject[NestedAdt] = deriveEncoder(renaming.snakeCase, None) - val codecForNestedAdt: Codec[NestedAdt] = deriveCodec(renaming.snakeCase, true, None) + val codecForNestedAdt: Codec[NestedAdt] = deriveCodec(renaming.snakeCase, true, None, false) object discriminator { val typeField = Some("_type") - implicit val decodeAdt: Decoder[Adt] = deriveDecoder(renaming.snakeCase, true, typeField) + implicit val decodeAdt: Decoder[Adt] = deriveDecoder(renaming.snakeCase, true, typeField, false) implicit val encodeAdt: Encoder.AsObject[Adt] = deriveEncoder(renaming.snakeCase, typeField) - val codecForAdt: Codec[Adt] = deriveCodec(renaming.snakeCase, true, typeField) + val codecForAdt: Codec[Adt] = deriveCodec(renaming.snakeCase, true, typeField, false) - implicit val decodeNestedAdt: Decoder[NestedAdt] = deriveDecoder(renaming.snakeCase, true, typeField) + implicit val decodeNestedAdt: Decoder[NestedAdt] = deriveDecoder(renaming.snakeCase, true, typeField, false) implicit val encodeNestedAdt: Encoder.AsObject[NestedAdt] = deriveEncoder(renaming.snakeCase, typeField) - val codecForNestedAdt: Codec[NestedAdt] = deriveCodec(renaming.snakeCase, true, typeField) + val codecForNestedAdt: Codec[NestedAdt] = deriveCodec(renaming.snakeCase, true, typeField, false) } } diff --git a/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesExample.scala b/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesExample.scala index 03ee72e2..04ec3560 100644 --- a/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesExample.scala +++ b/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesExample.scala @@ -20,8 +20,8 @@ object TransformMemberNamesExample { implicit val eqUser: Eq[User] = Eq.fromUniversalEquals implicit val encodeUser: Encoder[User] = deriveEncoder(renaming.snakeCase, None) - implicit val decodeUser: Decoder[User] = deriveDecoder(renaming.snakeCase, true, None) - val codecForUser: Codec[User] = deriveCodec(renaming.snakeCase, true, None) + implicit val decodeUser: Decoder[User] = deriveDecoder(renaming.snakeCase, true, None, false) + val codecForUser: Codec[User] = deriveCodec(renaming.snakeCase, true, None, false) } case class Role(title: String) @@ -31,7 +31,7 @@ object TransformMemberNamesExample { implicit val eqRole: Eq[Role] = Eq.fromUniversalEquals implicit val encodeRole: Encoder[Role] = deriveEncoder(_.toUpperCase, None) - implicit val decodeRole: Decoder[Role] = deriveDecoder(_.toUpperCase, true, None) + implicit val decodeRole: Decoder[Role] = deriveDecoder(_.toUpperCase, true, None, false) } case class Address(number: Int, street: String, city: String) @@ -50,7 +50,7 @@ object TransformMemberNamesExample { implicit val encodeAddress: Encoder[Address] = deriveEncoder(renaming.replaceWith("number" -> "#"), None) implicit val decodeAddress: Decoder[Address] = - deriveDecoder(renaming.replaceWith("number" -> "#"), true, None) + deriveDecoder(renaming.replaceWith("number" -> "#"), true, None, false) } case class Abc(a: String, b: String, c: String) @@ -67,6 +67,6 @@ object TransformMemberNamesExample { implicit val eqAbc: Eq[Abc] = Eq.fromUniversalEquals implicit val encodeAbc: Encoder[Abc] = deriveEncoder(_ => "x", None) - implicit val decodeAbc: Decoder[Abc] = deriveDecoder(_ => "x", true, None) + implicit val decodeAbc: Decoder[Abc] = deriveDecoder(_ => "x", true, None, false) } } diff --git a/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesSuite.scala b/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesSuite.scala index 4983c12a..7ef9be51 100644 --- a/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesSuite.scala +++ b/modules/derivation/shared/src/test/scala/io/circe/derivation/TransformMemberNamesSuite.scala @@ -7,26 +7,27 @@ import io.circe.syntax._ import io.circe.testing.CodecTests object TransformMemberNamesSuiteCodecs extends Serializable { - implicit val decodeFoo: Decoder[Foo] = deriveDecoder(renaming.snakeCase, true, None) + implicit val decodeFoo: Decoder[Foo] = deriveDecoder(renaming.snakeCase, true, None, false) implicit val encodeFoo: Encoder.AsObject[Foo] = deriveEncoder(renaming.snakeCase, None) - val codecForFoo: Codec.AsObject[Foo] = deriveCodec(renaming.snakeCase, true, None) + val codecForFoo: Codec.AsObject[Foo] = deriveCodec(renaming.snakeCase, true, None, false) - implicit val decodeBar: Decoder[Bar] = deriveDecoder(renaming.snakeCase, true, None) + implicit val decodeBar: Decoder[Bar] = deriveDecoder(renaming.snakeCase, true, None, false) implicit val encodeBar: Encoder.AsObject[Bar] = deriveEncoder(renaming.snakeCase, None) - val codecForBar: Codec.AsObject[Bar] = deriveCodec(renaming.snakeCase, true, None) + val codecForBar: Codec.AsObject[Bar] = deriveCodec(renaming.snakeCase, true, None, false) - implicit val decodeBaz: Decoder[Baz] = deriveDecoder(renaming.snakeCase, true, None) + implicit val decodeBaz: Decoder[Baz] = deriveDecoder(renaming.snakeCase, true, None, false) implicit val encodeBaz: Encoder.AsObject[Baz] = deriveEncoder(renaming.snakeCase, None) - val codecForBaz: Codec.AsObject[Baz] = deriveCodec(renaming.snakeCase, true, None) + val codecForBaz: Codec.AsObject[Baz] = deriveCodec(renaming.snakeCase, true, None, false) implicit def decodeQux[A: Decoder]: Decoder[Qux[A]] = - deriveDecoder(renaming.replaceWith("aa" -> "1", "bb" -> "2"), true, None) + deriveDecoder(renaming.replaceWith("aa" -> "1", "bb" -> "2"), true, None, false) implicit def encodeQux[A: Encoder]: Encoder.AsObject[Qux[A]] = deriveEncoder(renaming.replaceWith("aa" -> "1", "bb" -> "2"), None) def codecForQux[A: Decoder: Encoder]: Codec.AsObject[Qux[A]] = deriveCodec( renaming.replaceWith("aa" -> "1", "bb" -> "2"), true, - None + None, + false ) }