diff --git a/zio-schema-json/jvm/src/test/scala-2/zio/schema/codec/JsonCodecJVMSpec.scala b/zio-schema-json/jvm/src/test/scala-2/zio/schema/codec/JsonCodecJVMSpec.scala index 01f1da1c2..a1d3ac230 100644 --- a/zio-schema-json/jvm/src/test/scala-2/zio/schema/codec/JsonCodecJVMSpec.scala +++ b/zio-schema-json/jvm/src/test/scala-2/zio/schema/codec/JsonCodecJVMSpec.scala @@ -20,6 +20,18 @@ object JsonCodecJVMSpec extends ZIOSpecDefault { ) @@ TestAspect.jvmOnly @@ timeout(180.seconds) private val decoderSuite = suite("decoding")( + suite("decode record with more than 22 fields")( + test("missing fields in the json payload are populated with their default values") { + val exampleSchema = zio.schema.DeriveSchema.gen[RecordExample] + val string = """{"f1": "test"}""" + assertDecodesJson(exampleSchema, RecordExample(Some("test")), string) + }, + test("fail if a field with no default value is missing in the json payload") { + val exampleSchema = zio.schema.DeriveSchema.gen[RecordExample2] + val string = """{"f1": "test"}""" + assertDecodesJsonFailure(exampleSchema, string) + } + ), suite("case class")( test("case class with empty option field is decoded by stream") { val names = Gen.option(Gen.elements("John", "Jane", "Jermaine", "Jasmine")) @@ -50,6 +62,16 @@ object JsonCodecJVMSpec extends ZIOSpecDefault { ) ) + private def assertDecodesJson[A](schema: Schema[A], value: A, jsonString: String) = { + val either = JsonCodec.jsonDecoder(schema).decodeJson(jsonString) + zio.test.assert(either)(isRight(equalTo(value))) + } + + private def assertDecodesJsonFailure[A](schema: Schema[A], jsonString: String) = { + val either = JsonCodec.jsonDecoder(schema).decodeJson(jsonString) + zio.test.assertTrue(either.isLeft) + } + private def assertDecodesJsonStream[A]( schema: Schema[A], value: Chunk[A], @@ -64,4 +86,57 @@ object JsonCodecJVMSpec extends ZIOSpecDefault { .either assertZIO(result)(isRight(equalTo(value))) } + + case class RecordExample( + f1: Option[String], + f2: Option[String] = None, + f3: Option[String] = None, + f4: Option[String] = None, + f5: Option[String] = None, + f6: Option[String] = None, + f7: Option[String] = None, + f8: Option[String] = None, + f9: Option[String] = None, + f10: Option[String] = None, + f11: Option[String] = None, + f12: Option[String] = None, + f13: Option[String] = None, + f14: Option[String] = None, + f15: Option[String] = None, + f16: Option[String] = None, + f17: Option[String] = None, + f18: Option[String] = None, + f19: Option[String] = None, + f20: Option[String] = None, + f21: Option[String] = None, + f22: Option[String] = None, + f23: Option[String] = None + ) + + case class RecordExample2( + f1: Option[String], + f2: Option[String], + f3: Option[String] = None, + f4: Option[String] = None, + f5: Option[String] = None, + f6: Option[String] = None, + f7: Option[String] = None, + f8: Option[String] = None, + f9: Option[String] = None, + f10: Option[String] = None, + f11: Option[String] = None, + f12: Option[String] = None, + f13: Option[String] = None, + f14: Option[String] = None, + f15: Option[String] = None, + f16: Option[String] = None, + f17: Option[String] = None, + f18: Option[String] = None, + f19: Option[String] = None, + f20: Option[String] = None, + f21: Option[String] = None, + f22: Option[String] = None, + f23: Option[String] = None + ) + } diff --git a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala index 8aacace25..c11bea9f6 100644 --- a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala @@ -830,7 +830,18 @@ object JsonCodec { () } } - (ListMap.newBuilder[String, Any] ++= builder.result()).result() + val tuples = builder.result() + val collectedFields: Set[String] = tuples.map { case (fieldName, _) => fieldName }.toSet + val resultBuilder = ListMap.newBuilder[String, Any] + + // add fields with default values if they are not present in the JSON + structure.foreach { field => + if (!collectedFields.contains(field.name) && field.optional && field.defaultValue.isDefined) { + val value = field.name -> field.defaultValue.get + resultBuilder += value + } + } + (resultBuilder ++= tuples).result() } } diff --git a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala index 0393c8a7a..aeb062fb9 100644 --- a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala +++ b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala @@ -219,6 +219,13 @@ object JsonCodecSpec extends ZIOSpecDefault { } ), suite("record")( + test("missing fields should be replaced by default values") { + assertDecodes( + recordSchema, + ListMap[String, Any]("foo" -> "s", "bar" -> 1), + charSequenceToByteChunk("""{"foo":"s"}""") + ) + }, test("of primitives") { assertEncodes( recordSchema, @@ -435,6 +442,13 @@ object JsonCodecSpec extends ZIOSpecDefault { ListMap[String, Any]("foo" -> "s", "bar" -> 1), charSequenceToByteChunk("""{"foo":"s","bar":1,"baz":2}""") ) + }, + test("with missing fields") { + assertDecodes( + RecordExample.schema, + RecordExample(f1 = Some("test"), f2 = None), + charSequenceToByteChunk("""{"f1":"test"}""") + ) } ), suite("transform")( @@ -1636,6 +1650,7 @@ object JsonCodecSpec extends ZIOSpecDefault { .Field( "bar", Schema.Primitive(StandardType.IntType), + annotations0 = Chunk(fieldDefaultValue(1)), get0 = (p: ListMap[String, _]) => p("bar").asInstanceOf[Int], set0 = (p: ListMap[String, _], v: Int) => p.updated("bar", v) ) @@ -1867,4 +1882,35 @@ object JsonCodecSpec extends ZIOSpecDefault { object AllOptionalFields { implicit lazy val schema: Schema[AllOptionalFields] = DeriveSchema.gen[AllOptionalFields] } + + case class RecordExample( + f1: Option[String], // the only field that does not have a default value + f2: Option[String] = None, + f3: Option[String] = None, + f4: Option[String] = None, + f5: Option[String] = None, + f6: Option[String] = None, + f7: Option[String] = None, + f8: Option[String] = None, + f9: Option[String] = None, + f10: Option[String] = None, + f11: Option[String] = None, + f12: Option[String] = None, + f13: Option[String] = None, + f14: Option[String] = None, + f15: Option[String] = None, + f16: Option[String] = None, + f17: Option[String] = None, + f18: Option[String] = None, + f19: Option[String] = None, + f20: Option[String] = None, + f21: Option[String] = None, + f22: Option[String] = None, + f23: Option[String] = None + ) + + object RecordExample { + implicit lazy val schema: Schema[RecordExample] = DeriveSchema.gen[RecordExample] + } + }