From 0ef6321b880102bc583834d7af35f70c89c2e53a Mon Sep 17 00:00:00 2001 From: Antonio Morales Date: Tue, 17 Sep 2024 00:29:53 -0700 Subject: [PATCH] Add JsonCodec.Config.explicitNulls option (#740) --- .../scala/zio/schema/codec/JsonCodec.scala | 8 +++- .../zio/schema/codec/JsonCodecSpec.scala | 47 +++++++++++-------- 2 files changed, 33 insertions(+), 22 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..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 (!enc.isNothing(value) && !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 23d46c157..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 @@ -118,34 +118,42 @@ object JsonCodecSpec extends ZIOSpecDefault { suite("empty collections config")( test("list empty") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(Nil, Map("foo" -> 1)), - """{"map":{"foo":1}}""", - JsonCodec.Config(ignoreEmptyCollections = true) + Schema[ListAndMapAndOption], + ListAndMapAndOption(Nil, Map("foo" -> 1), Some("foo")), + """{"map":{"foo":1},"option":"foo"}""", + JsonCodec.Config(ignoreEmptyCollections = true, explicitNulls = false) ) }, test("map empty") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(List("foo"), Map.empty), - """{"list":["foo"]}""", - JsonCodec.Config(ignoreEmptyCollections = true) + Schema[ListAndMapAndOption], + ListAndMapAndOption(List("foo"), Map.empty, Some("foo")), + """{"list":["foo"],"option":"foo"}""", + JsonCodec.Config(ignoreEmptyCollections = true, explicitNulls = false) + ) + }, + test("option empty") { + assertEncodesJson( + Schema[ListAndMapAndOption], + ListAndMapAndOption(List("foo"), Map("foo" -> 1), None), + """{"list":["foo"],"map":{"foo":1}}""", + JsonCodec.Config(ignoreEmptyCollections = true, explicitNulls = false) ) }, test("all empty") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(Nil, Map.empty), + 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") { assertEncodesJson( - Schema[ListAndMap], - ListAndMap(Nil, Map.empty), - """{"list":[],"map":{}}""", - JsonCodec.Config(ignoreEmptyCollections = false) + Schema[ListAndMapAndOption], + ListAndMapAndOption(Nil, Map.empty, None), + """{"list":[],"map":{},"option":null}""", + JsonCodec.Config(ignoreEmptyCollections = false, explicitNulls = true) ) } ), @@ -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") { @@ -2159,10 +2166,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)