From ffe480817e0ecea50049312e8add541c88cd2f36 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 4 Jan 2025 01:04:16 +0100 Subject: [PATCH] Fix transient field inconsitency in JSON codecs for records (#764) * Clean up test name * Clean up test for decoding of case classes with transient field * Add missing test for transient field annotation with default value implicitly available for the field type * Clean up of tests * Add support for transient fields with implicitly available schema default values in Schema.GenericRecord --- .../scala/zio/schema/codec/JsonCodec.scala | 4 +- .../zio/schema/codec/JsonCodecSpec.scala | 54 +++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) 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 d65f0e7a7..e3876b796 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 @@ -995,7 +995,9 @@ object JsonCodec { val spanWithDecoder = (JsonError.ObjectAccess(fieldName), schemaDecoder(field.schema).asInstanceOf[ZJsonDecoder[Any]]) field.nameAndAliases.foreach(x => spansWithDecoders.put(x, spanWithDecoder)) - if (field.optional && field.defaultValue.isDefined) defaults.put(fieldName, field.defaultValue.get) + if ((field.optional || field.transient) && field.defaultValue.isDefined) { + defaults.put(fieldName, field.defaultValue.get) + } } val rejectAdditionalFields = schema.annotations.exists(_.isInstanceOf[rejectExtraFields]) (trace: List[JsonError], in: RetractReader) => { diff --git a/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala b/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala index 568f75c89..071695556 100644 --- a/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala +++ b/zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala @@ -303,7 +303,7 @@ object JsonCodecSpec extends ZIOSpecDefault { charSequenceToByteChunk("""{"oneOf":{"_type":"StringValue2","value":"foo2"}}""") ) }, - test("case class") { + test("transient field annotation") { assertEncodes( searchRequestWithTransientFieldSchema, SearchRequestWithTransientField("foo", 10, 20, "bar"), @@ -431,7 +431,7 @@ object JsonCodecSpec extends ZIOSpecDefault { suite("Generic Record")( test("Do not encode transient field") { assertEncodes( - RecordExample.schema.annotate(rejectExtraFields()), + RecordExample.schema, RecordExample(f1 = Some("test"), f3 = Some("transient")), charSequenceToByteChunk( """{"$f1":"test"}""".stripMargin @@ -812,13 +812,31 @@ object JsonCodecSpec extends ZIOSpecDefault { JsonError.Message("extra field") :: Nil ) }, - test("transient field annotation") { + test("transient field annotation with default value in class definition") { assertDecodes( searchRequestWithTransientFieldSchema, - SearchRequestWithTransientField("test", 0, 10, Schema[String].defaultValue.getOrElse("")), + SearchRequestWithTransientField("test", 0, 10), charSequenceToByteChunk("""{"query":"test","pageNumber":0,"resultPerPage":10}""") ) }, + test("transient field annotation with default value implicitly available for the field type") { + case class CaseClassWithTransientField(transient: String) + assertDecodes( + Schema.CaseClass1[String, CaseClassWithTransientField]( + id0 = TypeId.fromTypeName("SearchRequestWithTransientField"), + field0 = Schema.Field( + name0 = "transient", + schema0 = Schema[String], + get0 = _.transient, + set0 = (x, transient) => x.copy(transient = transient), + annotations0 = Chunk(new transientField()) + ), + defaultConstruct0 = new CaseClassWithTransientField(_) + ), + CaseClassWithTransientField(Schema[String].defaultValue.toOption.get), + charSequenceToByteChunk("""{}""") + ) + }, test("fieldDefaultValue") { assertDecodes( fieldDefaultValueSearchRequestSchema, @@ -854,6 +872,13 @@ object JsonCodecSpec extends ZIOSpecDefault { charSequenceToByteChunk("""{"foo":"s","bar":null}""") ) }, + test("with transient fields encoded as implicitly available schema default values") { + assertDecodes( + recordWithTransientSchema, + ListMap[String, Any]("foo" -> "", "bar" -> 0), + charSequenceToByteChunk("""{}""") + ) + }, test("case class with option fields encoded as null") { assertDecodes( WithOptionFields.schema, @@ -1991,7 +2016,7 @@ object JsonCodecSpec extends ZIOSpecDefault { query: String, pageNumber: Int, resultPerPage: Int, - @transientField nextPage: String = "" + @transientField nextPage: String = "transient" ) val searchRequestWithTransientFieldSchema: Schema[SearchRequestWithTransientField] = @@ -2035,6 +2060,25 @@ object JsonCodecSpec extends ZIOSpecDefault { ) ) + val recordWithTransientSchema: Schema[ListMap[String, _]] = Schema.record( + TypeId.Structural, + Schema.Field( + "foo", + Schema.Primitive(StandardType.StringType), + annotations0 = Chunk(transientField()), + get0 = (p: ListMap[String, _]) => p("foo").asInstanceOf[String], + set0 = (p: ListMap[String, _], v: String) => p.updated("foo", v) + ), + Schema + .Field( + "bar", + Schema.Primitive(StandardType.IntType), + annotations0 = Chunk(transientField()), + get0 = (p: ListMap[String, _]) => p("bar").asInstanceOf[Int], + set0 = (p: ListMap[String, _], v: Int) => p.updated("bar", v) + ) + ) + val nestedRecordSchema: Schema[ListMap[String, _]] = Schema.record( TypeId.Structural, Schema.Field(