diff --git a/README.md b/README.md index 2fc8b5b..cfcda07 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,8 @@ account.asJson == json ## Configuration + +### Configuration via **ReaderBuilder** and **WriterBuilder** 1. You can configure only case class derivation 2. To configure **JsonReader** use **ReaderBuilder** 3. To configure **JsonWriter** use **WriterBuilder** @@ -311,8 +313,102 @@ inline given ReaderBuilder[Foo] = case 2 => JsonReader[Int] case _ => JsonReader[Option[Boolean]] } + + // ensure that json contains only fields that JsonReader knows about, otherwise throw ReaderError + .strict +``` + +### Configuration via **JsonConfiguration** +1. To configure both **JsonWriter** and **JsonReader** you can use **JsonConfiguration** +2. **JsonConfiguration** can be provided as an inline given to derives +```scala 3 +inline given JsonConfiguration = JsonConfiguration.default +``` +3. **JsonConfiguration** will be applied recursively to all nested readers/writers + * Product types + ```scala 3 + import tethys.* + import tethys.jackson.* + + inline given JsonConfiguration = + JsonConfiguration.default.fieldStyle(FieldStyle.LowerSnakeCase) + + case class Inner(innerField: String) + case class Outer(outerField: Inner) derives JsonWriter, JsonReader + + val outer = Outer(Inner("fooBar")) + val json = """{"outer_field": {"inner_field": "fooBar"}}""" + + json.jsonAs[Outer] == Right(outer) + outer.asJson == json + + ``` + * Sum types + ```scala 3 + import tethys.* + import tethys.jackson.* + + inline given JsonConfiguration = + JsonConfiguration.default.fieldStyle(FieldStyle.LowerSnakeCase) + + enum Choice(@selector val select: Int) derives JsonReader, JsonWriter: + case First(firstField: Int) extends Choice(0) + case Second(secondField: String) extends Choice(1) + + val first = Choice.First(1) + val second = Choice.Second("foo") + val firstJson = """{"select": 0, "first_field": 1}""" + val secondJson = """{"select": 1, "second_field": "foo"}""" + + first.asJson == firstJson + second.asJson == secondJson + + firstJson.jsonAs[Choice] == first + secondJson.jsonAs[Choice] == second + ``` +4. **WriterBuilder** and **ReaderBuilder** settings have higher priority than **JsonConfiguration** settings +```scala 3 +import tethys.* +import tethys.jackson.* + +case class Customer( + id: Long, + phoneNumber: String +) derives JsonWriter, JsonReader + +inline given JsonConfiguration = + JsonConfiguration.default.fieldStyle(FieldStyle.LowerSnakeCase) + +inline given WriterBuilder[Customer] = + // has higher priority than JsonConfiguration's fieldStyle + WriterBuilder[Customer].fieldStyle(FieldStyle.UpperCase) + +inline given ReaderBuilder[Customer] = + // has higher priority than JsonConfiguration's fieldStyle + ReaderBuilder[Customer].fieldStyle(FieldStyle.UpperCase) + +val customer = Customer(id = 5L, phoneNumber = "+123") +val json = """{"ID": 5, "PHONENUMBER": "+123"}""" + +json.jsonAs[Customer] == Right(customer) +customer.asJson == json + ``` +5. **JsonConfiguration** features +```scala 3 +inline given JsonConfiguration = + JsonConfiguration + // default config, entrypoint for configuration + .default + + // choose field style + .fieldStyle(FieldStyle.UpperSnakeCase) + + // ensure that json contains only fields that JsonReader knows about, otherwise throw ReaderError + // applicable only for JsonReader + .strict +``` ## integrations In some cases, you may need to work with raw AST, diff --git a/modules/core/src/main/scala-3/tethys/JsonConfiguration.scala b/modules/core/src/main/scala-3/tethys/JsonConfiguration.scala new file mode 100644 index 0000000..97c688d --- /dev/null +++ b/modules/core/src/main/scala-3/tethys/JsonConfiguration.scala @@ -0,0 +1,12 @@ +package tethys + +trait JsonConfiguration: + def fieldStyle(fieldStyle: FieldStyle): JsonConfiguration + + def strict: JsonConfiguration + +object JsonConfiguration: + @scala.annotation.compileTimeOnly( + "JsonConfiguration should be declared as inline given" + ) + def default: JsonConfiguration = throw IllegalAccessException() diff --git a/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala b/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala index 7f57a5d..8bdc8bc 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala @@ -36,16 +36,41 @@ trait ConfigurationMacroUtils: case failure: ImplicitSearchFailure => None + private val FieldStyleAlreadyConfigured = "FieldStyle is already configured" + + private def showUnknownConfigTree(tree: String): String = + s"Unknown tree. Config must be an inlined given. \nTree: $tree" + + private def mergeWriterMacroConfigs( + writerBuilderConfig: WriterBuilderMacroConfig, + jsonConfig: WriterBuilderMacroConfig + ): WriterBuilderMacroConfig = + writerBuilderConfig.copy(fieldStyle = + writerBuilderConfig.fieldStyle.orElse(jsonConfig.fieldStyle) + ) + + private def mergeReaderMacroConfigs( + readerBuilderConfig: ReaderBuilderMacroConfig, + jsonConfig: ReaderBuilderMacroConfig + ): ReaderBuilderMacroConfig = + readerBuilderConfig.copy( + fieldStyle = readerBuilderConfig.fieldStyle.orElse(jsonConfig.fieldStyle), + isStrict = readerBuilderConfig.isStrict.orElse(jsonConfig.isStrict) + ) + def prepareWriterProductFields[T: Type]( - config: Expr[WriterBuilder[T]] + config: Expr[WriterBuilder[T]], + jsonConfig: Expr[JsonConfiguration] ): List[WriterField] = - val macroConfig = parseWriterBuilderMacroConfig[T](config) - val updates = macroConfig.update.map(it => it.name -> it).toMap + val writerConfig = parseWriterBuilderMacroConfig[T](config) + val parsedJsonConfig = parseWriterMacroJsonConfig(jsonConfig) + val mergedConfig = mergeWriterMacroConfigs(writerConfig, parsedJsonConfig) + val updates = mergedConfig.update.map(it => it.name -> it).toMap val tpe = TypeRepr.of[T] tpe.typeSymbol.caseFields.zipWithIndex - .filterNot((symbol, _) => macroConfig.delete(symbol.name)) + .filterNot((symbol, _) => mergedConfig.delete(symbol.name)) .collect { (symbol, idx) => - val name = macroConfig.fieldStyle.fold(symbol.name)( + val name = mergedConfig.fieldStyle.fold(symbol.name)( FieldStyle.applyStyle(symbol.name, _) ) updates.get(symbol.name) match @@ -65,7 +90,49 @@ trait ConfigurationMacroUtils: tpe = tpe.memberType(symbol), newName = None ) - } ::: macroConfig.add + } ::: mergedConfig.add + + private def parseWriterMacroJsonConfig( + config: Expr[JsonConfiguration] + ): WriterBuilderMacroConfig = { + @tailrec + def loop( + config: Expr[JsonConfiguration], + acc: WriterBuilderMacroConfig = WriterBuilderMacroConfig() + ): WriterBuilderMacroConfig = + config match + case '{ + JsonConfiguration.default + } => + acc + + case '{ + ($rest: JsonConfiguration).strict + } => + loop(rest, acc) + + case '{ + ($rest: JsonConfiguration).fieldStyle(${ fieldStyle }: FieldStyle) + } => + acc.fieldStyle match + case None => + loop(rest, acc.copy(fieldStyle = Some(fieldStyle.valueOrAbort))) + case Some(_) => + report.errorAndAbort(FieldStyleAlreadyConfigured) + + case other => + other.asTerm match + case Inlined(_, _, term) => + loop(term.asExprOf[JsonConfiguration]) + case _ => + report.errorAndAbort( + showUnknownConfigTree( + other.asTerm.show(using Printer.TreeStructure) + ) + ) + + loop(traverseTree(config.asTerm).asExprOf[JsonConfiguration]) + } private def parseWriterBuilderMacroConfig[T: Type]( config: Expr[WriterBuilder[T]] @@ -362,8 +429,9 @@ trait ConfigurationMacroUtils: loop(term.asExprOf[WriterBuilder[T]]) case _ => report.errorAndAbort( - s"Unknown tree. Config must be an inlined given.\nTree: ${other.asTerm - .show(using Printer.TreeStructure)}" + showUnknownConfigTree( + other.asTerm.show(using Printer.TreeStructure) + ) ) loop(traverseTree(config.asTerm).asExprOf[WriterBuilder[T]])._1 @@ -371,15 +439,18 @@ trait ConfigurationMacroUtils: end parseWriterBuilderMacroConfig def prepareReaderProductFields[T: Type]( - config: Expr[ReaderBuilder[T]] + config: Expr[ReaderBuilder[T]], + jsonConfig: Expr[JsonConfiguration] ): (List[ReaderField], IsStrict) = - val macroConfig = parseReaderBuilderMacroConfig[T](config) + val readerConfig = parseReaderBuilderMacroConfig[T](config) + val parsedJsonConfig = parseReaderMacroJsonConfig(jsonConfig) + val mergedConfig = mergeReaderMacroConfigs(readerConfig, parsedJsonConfig) val tpe = TypeRepr.of[T] val defaults = collectDefaults[T] val fields = tpe.typeSymbol.caseFields.zipWithIndex .map { case (symbol, idx) => val default = defaults.get(idx).map(_.asExprOf[Any]) - macroConfig.extracted.get(symbol.name) match + mergedConfig.extracted.get(symbol.name) match case Some(field: ReaderField.Basic) => val updatedDefault = field.extractor match case None => default @@ -392,15 +463,15 @@ trait ConfigurationMacroUtils: ) .map(_.asExprOf[Any]) - field.update(idx, updatedDefault, macroConfig.fieldStyle) + field.update(idx, updatedDefault, mergedConfig.fieldStyle) case Some(field) => - field.update(idx, default, macroConfig.fieldStyle) + field.update(idx, default, mergedConfig.fieldStyle) case None => ReaderField .Basic(symbol.name, tpe.memberType(symbol), None) - .update(idx, default, macroConfig.fieldStyle) + .update(idx, default, mergedConfig.fieldStyle) } val existingFieldNames = fields.map(_.name).toSet val additionalFields = fields @@ -420,7 +491,47 @@ trait ConfigurationMacroUtils: .distinctBy(_.name) val allFields = fields ::: additionalFields checkLoops(allFields) - (sortDependencies(allFields), macroConfig.isStrict) + (sortDependencies(allFields), mergedConfig.isStrict.getOrElse(false)) + + private def parseReaderMacroJsonConfig( + jsonConfig: Expr[JsonConfiguration] + ): ReaderBuilderMacroConfig = + @tailrec + def loop( + config: Expr[JsonConfiguration], + acc: ReaderBuilderMacroConfig = ReaderBuilderMacroConfig() + ): ReaderBuilderMacroConfig = + config match + case '{ + JsonConfiguration.default + } => + acc + + case '{ + ($rest: JsonConfiguration).strict + } => + acc.copy(isStrict = Some(true)) + + case '{ + ($rest: JsonConfiguration).fieldStyle($fieldStyle: FieldStyle) + } => + acc.fieldStyle match + case None => + loop(rest, acc.copy(fieldStyle = Some(fieldStyle.valueOrAbort))) + case Some(_) => + report.errorAndAbort(FieldStyleAlreadyConfigured) + + case other => + other.asTerm match + case Inlined(_, _, term) => + loop(term.asExprOf[JsonConfiguration]) + case _ => + report.errorAndAbort( + showUnknownConfigTree( + other.asTerm.show(using Printer.TreeStructure) + ) + ) + loop(traverseTree(jsonConfig.asTerm).asExprOf[JsonConfiguration]) private def sortDependencies(fields: List[ReaderField]): List[ReaderField] = val known = fields.map(_.name).toSet @@ -526,6 +637,7 @@ trait ConfigurationMacroUtils: s"Field '$name' exists in your model, use selector or .extract(_.$name).as[...] instead" ) + @tailrec def loop( config: Expr[ReaderBuilder[T]], acc: ReaderBuilderMacroConfig = ReaderBuilderMacroConfig(Map.empty) @@ -583,9 +695,10 @@ trait ConfigurationMacroUtils: case '{ ($rest: ReaderBuilder[T]).strict } => loop( config = rest, - acc = acc.copy(isStrict = true) + acc = acc.copy(isStrict = Some(true)) ) case other => + @tailrec def loopInner( term: Term, extractors: List[(String, TypeRepr)] = Nil, @@ -664,8 +777,9 @@ trait ConfigurationMacroUtils: ) case other => report.errorAndAbort( - s"Unknown tree. Config must be an inlined given.\nTree: ${other - .show(using Printer.TreeStructure)}" + showUnknownConfigTree( + other.show(using Printer.TreeStructure) + ) ) loopInner(other.asTerm) @@ -1056,7 +1170,7 @@ trait ConfigurationMacroUtils: case class ReaderBuilderMacroConfig( extracted: Map[String, ReaderField] = Map.empty, fieldStyle: Option[FieldStyle] = None, - isStrict: IsStrict = false + isStrict: Option[IsStrict] = None ): def withExtracted(field: ReaderField): ReaderBuilderMacroConfig = copy(extracted = extracted.updated(field.name, field)) diff --git a/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala b/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala index 300eacc..6666116 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala @@ -12,7 +12,8 @@ import tethys.{ JsonReader, JsonWriter, ReaderBuilder, - WriterBuilder + WriterBuilder, + JsonConfiguration } import scala.Tuple2 import scala.annotation.tailrec @@ -24,17 +25,25 @@ import scala.deriving.Mirror private[tethys] object Derivation: inline def deriveJsonWriterForProduct[T]( - inline config: WriterBuilder[T] + inline config: WriterBuilder[T], + inline jsonConfig: JsonConfiguration ): JsonObjectWriter[T] = - ${ DerivationMacro.deriveJsonWriterForProduct[T]('{ config }) } + ${ + DerivationMacro + .deriveJsonWriterForProduct[T]('{ config }, '{ jsonConfig }) + } inline def deriveJsonWriterForSum[T]: JsonObjectWriter[T] = ${ DerivationMacro.deriveJsonWriterForSum[T] } inline def deriveJsonReaderForProduct[T]( - inline config: ReaderBuilder[T] + inline config: ReaderBuilder[T], + inline jsonConfig: JsonConfiguration ): JsonReader[T] = - ${ DerivationMacro.deriveJsonReaderForProduct[T]('{ config }) } + ${ + DerivationMacro + .deriveJsonReaderForProduct[T]('{ config }, '{ jsonConfig }) + } @deprecated inline def deriveJsonReaderForProductLegacy[T]( @@ -64,20 +73,28 @@ private[tethys] object Derivation: ${ DerivationMacro.deriveJsonReaderForSum[T] } object DerivationMacro: - def deriveJsonWriterForProduct[T: Type](config: Expr[WriterBuilder[T]])(using + def deriveJsonWriterForProduct[T: Type]( + config: Expr[WriterBuilder[T]], + jsonConfig: Expr[JsonConfiguration] + )(using quotes: Quotes ): Expr[JsonObjectWriter[T]] = - new DerivationMacro(quotes).deriveJsonWriterForProduct[T](config) + new DerivationMacro(quotes) + .deriveJsonWriterForProduct[T](config, jsonConfig) def deriveJsonWriterForSum[T: Type](using quotes: Quotes ): Expr[JsonObjectWriter[T]] = new DerivationMacro(quotes).deriveJsonWriterForSum[T](None) - def deriveJsonReaderForProduct[T: Type](config: Expr[ReaderBuilder[T]])(using + def deriveJsonReaderForProduct[T: Type]( + config: Expr[ReaderBuilder[T]], + jsonConfig: Expr[JsonConfiguration] + )(using quotes: Quotes ): Expr[JsonReader[T]] = - new DerivationMacro(quotes).deriveJsonReaderForProduct[T](config) + new DerivationMacro(quotes) + .deriveJsonReaderForProduct[T](config, jsonConfig) def deriveJsonReaderForSum[T: Type](using quotes: Quotes @@ -111,9 +128,10 @@ private[derivation] class DerivationMacro(val quotes: Quotes) import quotes.reflect.* def deriveJsonWriterForProduct[T: Type]( - config: Expr[WriterBuilder[T]] + config: Expr[WriterBuilder[T]], + jsonConfig: Expr[JsonConfiguration] ): Expr[JsonObjectWriter[T]] = - val fields = prepareWriterProductFields(config) + val fields = prepareWriterProductFields(config, jsonConfig) val (missingWriters, refs) = deriveMissingWriters(TypeRepr.of[T], fields.map(_.tpe)) val writer = Block( @@ -352,10 +370,11 @@ private[derivation] class DerivationMacro(val quotes: Quotes) ).asExprOf[Unit] def deriveJsonReaderForProduct[T: Type]( - config: Expr[ReaderBuilder[T]] + config: Expr[ReaderBuilder[T]], + jsonConfig: Expr[JsonConfiguration] ): Expr[JsonReader[T]] = val tpe = TypeRepr.of[T] - val (fields, isStrict) = prepareReaderProductFields[T](config) + val (fields, isStrict) = prepareReaderProductFields[T](config, jsonConfig) val existingLabels = fields.map(_.name).toSet val fieldsWithoutReader = fields.collect { case field: ReaderField.Extracted if field.reader => field.name @@ -626,7 +645,8 @@ private[derivation] class DerivationMacro(val quotes: Quotes) mirror: Expr[Mirror.ProductOf[T]] ): Expr[JsonReader[T]] = deriveJsonReaderForProduct( - parseLegacyReaderDerivationConfig(config, mirror) + parseLegacyReaderDerivationConfig(config, mirror), + '{ JsonConfiguration.default } ) @deprecated @@ -635,7 +655,8 @@ private[derivation] class DerivationMacro(val quotes: Quotes) mirror: Expr[Mirror.ProductOf[T]] ): Expr[JsonObjectWriter[T]] = deriveJsonWriterForProduct( - parseLegacyWriterDerivationConfig(config, mirror) + parseLegacyWriterDerivationConfig(config, mirror), + '{ JsonConfiguration.default } ) @deprecated diff --git a/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala b/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala index cff0a8f..95f7b7b 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/JsonObjectWriterDerivation.scala @@ -3,7 +3,7 @@ package tethys.derivation import tethys.derivation.builder.WriterDerivationConfig import scala.deriving.Mirror -import tethys.{JsonObjectWriter, JsonWriter, WriterBuilder} +import tethys.{JsonObjectWriter, JsonWriter, WriterBuilder, JsonConfiguration} import tethys.writers.tokens.TokenWriter import scala.deriving.Mirror @@ -19,7 +19,7 @@ private[tethys] trait JsonObjectWriterDerivation: inline def derived[A](inline config: WriterBuilder[A])(using mirror: Mirror.ProductOf[A] ) = - Derivation.deriveJsonWriterForProduct[A](config) + Derivation.deriveJsonWriterForProduct[A](config, JsonConfiguration.default) @deprecated("Use WriterBuilder instead") inline def derived[A](inline config: WriterDerivationConfig)(using @@ -40,6 +40,12 @@ private[tethys] trait JsonObjectWriterDerivation: case config: WriterBuilder[A] => config case _ => WriterBuilder[A] + }, + summonFrom[JsonConfiguration] { + case jsonConfig: JsonConfiguration => + jsonConfig + case _ => JsonConfiguration.default + } ) diff --git a/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala b/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala index 36b957f..013eb5d 100644 --- a/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala +++ b/modules/core/src/main/scala-3/tethys/derivation/JsonReaderDerivation.scala @@ -5,6 +5,7 @@ import tethys.readers.tokens.{QueueIterator, TokenIterator} import tethys.readers.{FieldName, ReaderError} import tethys.JsonReader import tethys.ReaderBuilder +import tethys.JsonConfiguration import tethys.derivation.builder.ReaderDerivationConfig import scala.collection.mutable @@ -33,7 +34,7 @@ private[tethys] trait JsonReaderDerivation: inline def derived[A](inline config: ReaderBuilder[A])(using mirror: Mirror.ProductOf[A] ): JsonReader[A] = - Derivation.deriveJsonReaderForProduct[A](config) + Derivation.deriveJsonReaderForProduct[A](config, JsonConfiguration.default) @deprecated("Use ReaderBuilder instead") inline def derived[A](inline config: ReaderDerivationConfig)(using @@ -48,6 +49,10 @@ private[tethys] trait JsonReaderDerivation: summonFrom[ReaderBuilder[A]] { case config: ReaderBuilder[A] => config case _ => ReaderBuilder[A] + }, + summonFrom[JsonConfiguration] { + case config: JsonConfiguration => config + case _ => JsonConfiguration.default } ) case given Mirror.SumOf[A] => diff --git a/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala b/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala index 5571234..27c1553 100644 --- a/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala +++ b/modules/core/src/test/scala-3/tethys/derivation/DerivationSpec.scala @@ -822,4 +822,98 @@ class DerivationSpec extends AnyFlatSpec with Matchers { ) shouldBe SubChild(3) } + it should "apply configuration for multiple case classes" in { + + inline given JsonConfiguration = JsonConfiguration.default + .fieldStyle(FieldStyle.LowerSnakeCase) + + case class First(firstField: String) derives JsonWriter, JsonReader + case class Second(secondField: String) derives JsonWriter, JsonReader + + val first = First("abc") + val second = Second("bcd") + val firstJson = obj("first_field" -> "abc") + val secondJson = obj("second_field" -> "bcd") + + first.asTokenList shouldBe firstJson + second.asTokenList shouldBe secondJson + + read[First](firstJson) shouldBe first + read[Second](secondJson) shouldBe second + } + + it should "apply configuration when derive product recursively" in { + inline given JsonConfiguration = JsonConfiguration.default + .fieldStyle(FieldStyle.LowerSnakeCase) + + case class Inner(innerField: String) + case class Outer(outerField: Inner) derives JsonWriter, JsonReader + + val model = Outer(Inner("foo")) + val json = obj("outer_field" -> obj("inner_field" -> "foo")) + + model.asTokenList shouldBe json + read[Outer](json) shouldBe model + } + + it should "apply configuration when derive sum recursively" in { + inline given JsonConfiguration = JsonConfiguration.default + .fieldStyle(FieldStyle.LowerSnakeCase) + + enum Choice(@selector val select: Int) derives JsonReader, JsonWriter: + case First(firstField: Int) extends Choice(0) + case Second(secondField: String) extends Choice(1) + + val first = Choice.First(1) + val second = Choice.Second("foo") + val firstJson = obj("select" -> 0, "first_field" -> 1) + val secondJson = obj("select" -> 1, "second_field" -> "foo") + + first.asTokenList shouldBe firstJson + second.asTokenList shouldBe secondJson + + read[Choice](firstJson) shouldBe first + read[Choice](secondJson) shouldBe second + } + + it should "select Writer/Reader configuration over JsonConfiguration as more specific" in { + + inline given WriterBuilder[Outer] = + WriterBuilder[Outer].fieldStyle(FieldStyle.UpperCase) + + inline given ReaderBuilder[Outer] = + ReaderBuilder[Outer].fieldStyle(FieldStyle.UpperCase) + + inline given JsonConfiguration = + JsonConfiguration.default.fieldStyle(FieldStyle.LowerSnakeCase) + + case class Inner(innerField: String) + case class Outer(outerField: Inner) derives JsonWriter, JsonReader + + val model = Outer(Inner("foo")) + val json = obj("OUTERFIELD" -> obj("inner_field" -> "foo")) + + model.asTokenList shouldBe json + read[Outer](json) shouldBe model + + } + + it should "respect strict JsonConfiguration setting" in { + + inline given JsonReader[SimpleType] = JsonReader.derived[SimpleType] + + inline given JsonConfiguration = + JsonConfiguration.default.strict + + val json = + obj("i" -> 5, "s" -> "foo", "d" -> 5.5, "anotherField" -> "bar") + + (the[ReaderError] thrownBy { + read[SimpleType]( + json + ) + }).getMessage shouldBe "Illegal json at '[ROOT]': unexpected field 'anotherField', expected one of 'i', 's', 'd'" + + } + }