Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement strict decoding #213

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ private[magnolia] object MagnoliaDecoder {
throw new DerivationError("Duplicate key detected after applying transformation function for case class parameters")
}

if (configuration.useDefaults) {
val nonStrictDecoder = if (configuration.useDefaults) {
new Decoder[T] {
override def apply(c: HCursor): Result[T] = {
caseClass.constructEither { p =>
Expand All @@ -52,6 +52,26 @@ private[magnolia] object MagnoliaDecoder {
}.leftMap(_.head)
}
}

if (configuration.strictDecoding) {
val expectedFields = paramJsonKeyLookup.values
val strictDecoder = nonStrictDecoder.validate { cursor: 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."))
}
(c: HCursor) => strictDecoder(c)
} else {
nonStrictDecoder
}
}

private[magnolia] def dispatch[T](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import java.util.regex.Pattern
* formatting or case changes.
* If there are collisions in transformed constructor names, an exception will be thrown
* during derivation (runtime)
* @param strictDecoding When true, raises a decoding error when there are any extraneous fields in the given JSON
* that aren't present in the case class.
*/
final case class Configuration(
transformMemberNames: String => String,
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
discriminator: Option[String],
strictDecoding: Boolean = false
) {
def withSnakeCaseMemberNames: Configuration = copy(
transformMemberNames = Configuration.snakeCaseTransformation
Expand All @@ -44,6 +47,7 @@ final case class Configuration(

def withDefaults: Configuration = copy(useDefaults = true)
def withDiscriminator(discriminator: String): Configuration = copy(discriminator = Some(discriminator))
def withStrictDecoding: Configuration = copy(strictDecoding = true)
}

final object Configuration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class ConfiguredAutoDerivedEquivalenceSuite extends CirceSuite {
testWithConfiguration("with snake case configuration", Configuration.default.withSnakeCaseConstructorNames.withSnakeCaseMemberNames)
testWithConfiguration("with useDefault = true", Configuration.default.copy(useDefaults = true))
testWithConfiguration("with discriminator", Configuration.default.copy(discriminator = Some("type")))
testWithConfiguration("with strict", Configuration.default.copy(strictDecoding = true))

"If a sealed trait subtype has explicit Encoder instance that doesn't encode to a JsonObject, the derived encoder" should
s"wrap it with type constructor even when discriminator is specified by the configuration" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class ConfiguredSemiautoDerivedEquivalenceSuite extends CirceSuite {
testWithConfiguration("with snake case configuration", Configuration.default.withSnakeCaseConstructorNames.withSnakeCaseMemberNames)
testWithConfiguration("with useDefault = true", Configuration.default.copy(useDefaults = true))
testWithConfiguration("with discriminator", Configuration.default.copy(discriminator = Some("type")))
testWithConfiguration("with strict", Configuration.default.copy(strictDecoding = true))

"If a sealed trait subtype has explicit Encoder instance that doesn't encode to a JsonObject, the derived encoder" should
s"wrap it with type constructor even when discriminator is specified by the configuration" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ package io.circe.magnolia.configured

import io.circe.magnolia.DerivationError
import io.circe._
import io.circe.magnolia.configured.ConfiguredSemiautoDerivedSuite.{
DefaultConfig,
KebabCase,
SnakeCaseAndDiscriminator,
WithDefaultValue
}
import io.circe.magnolia.configured.ConfiguredSemiautoDerivedSuite.{DefaultConfig, KebabCase, Lenient, SnakeCaseAndDiscriminator, Strict, WithDefaultValue}
import io.circe.tests.CirceSuite
import io.circe.tests.examples.{Bar, ClassWithDefaults, ClassWithJsonKey, NonProfit, Organization, Public}
import org.scalatest.Inside
Expand Down Expand Up @@ -192,6 +187,38 @@ class ConfiguredSemiautoDerivedSuite extends CirceSuite with Inside {
)
}

"Configuration#strictDecoding" should "Raise error when strict decoding enabled and extraneous key is found in JSON" in {
val input = parse("""
{
"NonProfit": {
"orgName": "RSPCA",
"extraneous": true
}
}
""")
inside(input.flatMap(i => Strict.decoder(i.hcursor))) {
case Left(e: DecodingFailure) => {
assert(e.message.contains("Unexpected field"))
assert(e.message.contains("extraneous"))
assert(e.message.contains("orgName"))
}
case x => fail(x.toString)
}
}

"Configuration#strictDecoding" should "Should not raise error when strict decoding is disabled and extraneous key is found in JSON" in {
val input = parse("""
{
"NonProfit": {
"orgName": "RSPCA",
"extraneous": true
}
}
""")
val expected = NonProfit("RSPCA")
assert(input.flatMap(i => Lenient.decoder(i.hcursor)) == Right(expected))
}

"Encoder derivation" should "fail if transforming parameter names has collisions" in {
implicit val config: Configuration = Configuration.default.copy(transformMemberNames = _ => "sameKey")

Expand Down Expand Up @@ -279,4 +306,18 @@ object ConfiguredSemiautoDerivedSuite {
val decoder: Decoder[Organization] = deriveConfiguredMagnoliaDecoder[Organization]
}

object Strict {
implicit val configuration: Configuration = Configuration.default.withStrictDecoding

val encoder: Encoder[Organization] = deriveConfiguredMagnoliaEncoder[Organization]
val decoder: Decoder[Organization] = deriveConfiguredMagnoliaDecoder[Organization]
}

object Lenient {
implicit val configuration: Configuration = Configuration.default

val encoder: Encoder[Organization] = deriveConfiguredMagnoliaEncoder[Organization]
val decoder: Decoder[Organization] = deriveConfiguredMagnoliaDecoder[Organization]
}

}