From ddc29af9d446d242555c7ece2f927d6ae3e7007b Mon Sep 17 00:00:00 2001 From: Antonio Morales Date: Thu, 12 Sep 2024 11:39:53 -0700 Subject: [PATCH 1/2] make JsonCodec.Config.ignoreEmptyCollections work with Options --- .../scala/zio/schema/codec/JsonCodec.scala | 2 +- .../zio/schema/codec/JsonCodecSpec.scala | 48 +++++++++++-------- 2 files changed, 29 insertions(+), 21 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 38ea34ff6..00440b588 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 @@ -1089,7 +1089,7 @@ object JsonCodec { case e: Throwable => throw new RuntimeException(s"Failed to encode field '${s.name}' in $schema'", e) } val value = s.get(a) - if (!enc.isNothing(value) && !isEmptyOptionalValue(s, value, cfg)) { + if (!isEmptyOptionalValue(s, value, cfg)) { if (first) first = false else { 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 23d46c157..47487ab52 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 @@ -8,7 +8,7 @@ import zio.Console._ import zio._ import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.{ DeriveJsonEncoder, JsonEncoder } +import zio.json.{ DeriveJsonEncoder, JsonEncoder, jsonExplicitNull } import zio.schema.CaseSet._ import zio.schema._ import zio.schema.annotation._ @@ -118,33 +118,41 @@ object JsonCodecSpec extends ZIOSpecDefault { suite("empty collections config")( test("list empty") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(Nil, Map("foo" -> 1)), - """{"map":{"foo":1}}""", + Schema[ListAndMapAndOption], + ListAndMapAndOption(Nil, Map("foo" -> 1), Some("foo")), + """{"map":{"foo":1},"option":"foo"}""", JsonCodec.Config(ignoreEmptyCollections = true) ) }, test("map empty") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(List("foo"), Map.empty), - """{"list":["foo"]}""", + Schema[ListAndMapAndOption], + ListAndMapAndOption(List("foo"), Map.empty, Some("foo")), + """{"list":["foo"],"option":"foo"}""", + JsonCodec.Config(ignoreEmptyCollections = true) + ) + }, + test("option empty") { + assertEncodesJson( + Schema[ListAndMapAndOption], + ListAndMapAndOption(List("foo"), Map("foo" -> 1), None), + """{"list":["foo"],"map":{"foo":1}}""", JsonCodec.Config(ignoreEmptyCollections = true) ) }, test("all empty") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(Nil, Map.empty), + Schema[ListAndMapAndOption], + ListAndMapAndOption(Nil, Map.empty, None), """{}""", JsonCodec.Config(ignoreEmptyCollections = true) ) }, test("all empty, but don't ignore empty collections") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(Nil, Map.empty), - """{"list":[],"map":{}}""", + Schema[ListAndMapAndOption], + ListAndMapAndOption(Nil, Map.empty, None), + """{"list":[],"map":{},"option":null}""", JsonCodec.Config(ignoreEmptyCollections = false) ) } @@ -269,7 +277,7 @@ object JsonCodecSpec extends ZIOSpecDefault { assertEncodes( WithOptionFields.schema, WithOptionFields(Some("s"), None), - charSequenceToByteChunk("""{"a":"s"}""") + charSequenceToByteChunk("""{"a":"s","b":null}""") ) } ), @@ -327,7 +335,7 @@ object JsonCodecSpec extends ZIOSpecDefault { assertEncodes( Subscription.schema, Subscription.Unlimited(None), - charSequenceToByteChunk("""{"type":"unlimited"}""") + charSequenceToByteChunk("""{"type":"unlimited","until":null}""") ) }, suite("with no discriminator")( @@ -871,7 +879,7 @@ object JsonCodecSpec extends ZIOSpecDefault { assertDecodes( WithOptionFields.schema, WithOptionFields(Some("s"), None), - charSequenceToByteChunk("""{"a":"s"}""") + charSequenceToByteChunk("""{"a":"s","b":null}""") ) }, test("case class with option fields accept empty json object as value") { @@ -1510,8 +1518,7 @@ object JsonCodecSpec extends ZIOSpecDefault { Enumeration3(StringValue3("foo")) ) &> assertEncodesThenDecodes( Schema[Enumeration3], - Enumeration3(StringValue3Multi("foo", "bar")), - print = true + Enumeration3(StringValue3Multi("foo", "bar")) ) }, test("of case classes with discriminator") { @@ -1878,6 +1885,7 @@ object JsonCodecSpec extends ZIOSpecDefault { charSequenceToByteChunk(encoded) } + @jsonExplicitNull case class SearchRequest(query: String, pageNumber: Int, resultPerPage: Int, nextPage: Option[String]) object SearchRequest { @@ -2159,10 +2167,10 @@ object JsonCodecSpec extends ZIOSpecDefault { implicit lazy val schema: Schema[WithOptField] = DeriveSchema.gen[WithOptField] } - final case class ListAndMap(list: List[String], map: Map[String, Int]) + final case class ListAndMapAndOption(list: List[String], map: Map[String, Int], option: Option[String]) - object ListAndMap { - implicit lazy val schema: Schema[ListAndMap] = DeriveSchema.gen[ListAndMap] + object ListAndMapAndOption { + implicit lazy val schema: Schema[ListAndMapAndOption] = DeriveSchema.gen[ListAndMapAndOption] } final case class KeyWrapper(key: String) From c1a19554464215713cde9c687af30f49d3be96c9 Mon Sep 17 00:00:00 2001 From: Antonio Morales Date: Thu, 12 Sep 2024 12:17:19 -0700 Subject: [PATCH 2/2] use different config value --- .../scala/zio/schema/codec/JsonCodec.scala | 8 ++++++-- .../zio/schema/codec/JsonCodecSpec.scala | 19 +++++++++---------- 2 files changed, 15 insertions(+), 12 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 00440b588..ee9c14a3d 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 @@ -26,7 +26,11 @@ import zio.{ Cause, Chunk, ChunkBuilder, ZIO, ZNothing } object JsonCodec { - final case class Config(ignoreEmptyCollections: Boolean, treatStreamsAsArrays: Boolean = false) + final case class Config( + ignoreEmptyCollections: Boolean, + treatStreamsAsArrays: Boolean = false, + explicitNulls: Boolean = false + ) object Config { val default: Config = Config(ignoreEmptyCollections = false) @@ -1089,7 +1093,7 @@ object JsonCodec { case e: Throwable => throw new RuntimeException(s"Failed to encode field '${s.name}' in $schema'", e) } val value = s.get(a) - if (!isEmptyOptionalValue(s, value, cfg)) { + if (!isEmptyOptionalValue(s, value, cfg) && (!enc.isNothing(value) || cfg.explicitNulls)) { if (first) first = false else { 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 47487ab52..0d3e37175 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 @@ -8,7 +8,7 @@ import zio.Console._ import zio._ import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.{ DeriveJsonEncoder, JsonEncoder, jsonExplicitNull } +import zio.json.{ DeriveJsonEncoder, JsonEncoder } import zio.schema.CaseSet._ import zio.schema._ import zio.schema.annotation._ @@ -121,7 +121,7 @@ object JsonCodecSpec extends ZIOSpecDefault { Schema[ListAndMapAndOption], ListAndMapAndOption(Nil, Map("foo" -> 1), Some("foo")), """{"map":{"foo":1},"option":"foo"}""", - JsonCodec.Config(ignoreEmptyCollections = true) + JsonCodec.Config(ignoreEmptyCollections = true, explicitNulls = false) ) }, test("map empty") { @@ -129,7 +129,7 @@ object JsonCodecSpec extends ZIOSpecDefault { Schema[ListAndMapAndOption], ListAndMapAndOption(List("foo"), Map.empty, Some("foo")), """{"list":["foo"],"option":"foo"}""", - JsonCodec.Config(ignoreEmptyCollections = true) + JsonCodec.Config(ignoreEmptyCollections = true, explicitNulls = false) ) }, test("option empty") { @@ -137,7 +137,7 @@ object JsonCodecSpec extends ZIOSpecDefault { Schema[ListAndMapAndOption], ListAndMapAndOption(List("foo"), Map("foo" -> 1), None), """{"list":["foo"],"map":{"foo":1}}""", - JsonCodec.Config(ignoreEmptyCollections = true) + JsonCodec.Config(ignoreEmptyCollections = true, explicitNulls = false) ) }, test("all empty") { @@ -145,7 +145,7 @@ object JsonCodecSpec extends ZIOSpecDefault { Schema[ListAndMapAndOption], ListAndMapAndOption(Nil, Map.empty, None), """{}""", - JsonCodec.Config(ignoreEmptyCollections = true) + JsonCodec.Config(ignoreEmptyCollections = true, explicitNulls = false) ) }, test("all empty, but don't ignore empty collections") { @@ -153,7 +153,7 @@ object JsonCodecSpec extends ZIOSpecDefault { Schema[ListAndMapAndOption], ListAndMapAndOption(Nil, Map.empty, None), """{"list":[],"map":{},"option":null}""", - JsonCodec.Config(ignoreEmptyCollections = false) + JsonCodec.Config(ignoreEmptyCollections = false, explicitNulls = true) ) } ), @@ -277,7 +277,7 @@ object JsonCodecSpec extends ZIOSpecDefault { assertEncodes( WithOptionFields.schema, WithOptionFields(Some("s"), None), - charSequenceToByteChunk("""{"a":"s","b":null}""") + charSequenceToByteChunk("""{"a":"s"}""") ) } ), @@ -335,7 +335,7 @@ object JsonCodecSpec extends ZIOSpecDefault { assertEncodes( Subscription.schema, Subscription.Unlimited(None), - charSequenceToByteChunk("""{"type":"unlimited","until":null}""") + charSequenceToByteChunk("""{"type":"unlimited"}""") ) }, suite("with no discriminator")( @@ -879,7 +879,7 @@ object JsonCodecSpec extends ZIOSpecDefault { assertDecodes( WithOptionFields.schema, WithOptionFields(Some("s"), None), - charSequenceToByteChunk("""{"a":"s","b":null}""") + charSequenceToByteChunk("""{"a":"s"}""") ) }, test("case class with option fields accept empty json object as value") { @@ -1885,7 +1885,6 @@ object JsonCodecSpec extends ZIOSpecDefault { charSequenceToByteChunk(encoded) } - @jsonExplicitNull case class SearchRequest(query: String, pageNumber: Int, resultPerPage: Int, nextPage: Option[String]) object SearchRequest {