From e482c359e2d877bd1fcd7f083581ac9ef5f57015 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Thu, 2 May 2024 10:01:26 +0200 Subject: [PATCH 1/2] Ways to configure the explicit encoding of empty options as null (#1085) --- README.md | 4 ++-- .../src/main/scala-2.x/zio/json/macros.scala | 16 ++++++++++++---- .../src/main/scala-3/zio/json/macros.scala | 11 +++++++++-- .../zio/json/ConfigurableDeriveCodecSpec.scala | 17 +++++++++++++++++ .../src/test/scala/zio/json/EncoderSpec.scala | 10 ++++++++++ 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1018ffe9..17104468 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [ZIO Json](https://github.com/zio/zio-json) is a fast and secure JSON library with tight ZIO integration. -[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-json/workflows/CI/badge.svg) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) +[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-json/workflows/CI/badge.svg) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-json_2.13/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-json-docs_2.13/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-json-docs_2.13) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) ## Introduction @@ -25,7 +25,7 @@ The goal of this project is to create the best all-round JSON library for Scala: In order to use this library, we need to add the following line in our `build.sbt` file: ```scala -libraryDependencies += "dev.zio" %% "zio-json" % "" +libraryDependencies += "dev.zio" %% "zio-json" % "0.6.2" ``` ## Example diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 823687d2..42fffbb9 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -22,6 +22,8 @@ final case class jsonField(name: String) extends Annotation */ final case class jsonAliases(alias: String, aliases: String*) extends Annotation +final class jsonExplicitNull extends Annotation + /** * If used on a sealed class, will determine the name of the field for * disambiguating classes. @@ -212,7 +214,8 @@ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, fieldNameMapping: JsonMemberFormat = IdentityFormat, allowExtraFields: Boolean = true, - sumTypeMapping: JsonMemberFormat = IdentityFormat + sumTypeMapping: JsonMemberFormat = IdentityFormat, + explicitNulls: Boolean = false ) object JsonCodecConfiguration { @@ -554,6 +557,10 @@ object DeriveJsonEncoder { name }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) } + + val explicitNulls: Boolean = + config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + lazy val tcs: Array[JsonEncoder[Any]] = params.map(p => p.typeclass.asInstanceOf[JsonEncoder[Any]]) val len: Int = params.length def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { @@ -564,9 +571,10 @@ object DeriveJsonEncoder { var prevFields = false // whether any fields have been written while (i < len) { - val tc = tcs(i) - val p = params(i).dereference(a) - if (!tc.isNothing(p)) { + val tc = tcs(i) + val p = params(i).dereference(a) + val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) + if (!tc.isNothing(p) || writeNulls) { // if we have at least one field already, we need a comma if (prevFields) { if (indent.isEmpty) out.write(",") diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index d6150090..301a0242 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -27,6 +27,11 @@ final case class jsonField(name: String) extends Annotation */ final case class jsonAliases(alias: String, aliases: String*) extends Annotation +/** + * Empty option fields will be encoded as `null`. + */ +final class jsonExplicitNull extends Annotation + /** * If used on a sealed class, will determine the name of the field for * disambiguating classes. @@ -540,6 +545,8 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => }) .toArray + val explicitNulls = ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + lazy val tcs: Array[JsonEncoder[Any]] = IArray.genericWrapArray(params.map(_.typeclass.asInstanceOf[JsonEncoder[Any]])).toArray @@ -555,8 +562,8 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => while (i < len) { val tc = tcs(i) val p = params(i).deref(a) - - if (! tc.isNothing(p)) { + val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) + if (! tc.isNothing(p) || writeNulls) { // if we have at least one field already, we need a comma if (prevFields) { if (indent.isEmpty) { diff --git a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala index 4f2094c0..2cafc819 100644 --- a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala @@ -14,6 +14,8 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { case class CaseClass(i: Int) extends ST } + case class OptionalField(a: Option[Int]) + def spec = suite("ConfigurableDeriveCodecSpec")( suite("defaults")( suite("string")( @@ -177,6 +179,21 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { ) } ) + ), + suite("explicit nulls")( + test("write null if configured") { + val expectedStr = """{"a":null}""" + val expectedObj = OptionalField(None) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + } ) ) } diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index c8cd8579..f8b895ec 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -291,6 +291,7 @@ object EncoderSpec extends ZIOSpecDefault { ) && assert(CoupleOfThings(0, None, true).toJsonPretty)(equalTo("{\n \"j\" : 0,\n \"b\" : true\n}")) && assert(OptionalAndRequired(None, "foo").toJson)(equalTo("""{"s":"foo"}""")) + assert(OptionalExplicitNullAndRequired(None, "foo").toJson)(equalTo("""{"i":null,"s":"foo"}""")) }, test("sum encoding") { import examplesum._ @@ -468,6 +469,15 @@ object EncoderSpec extends ZIOSpecDefault { DeriveJsonEncoder.gen[OptionalAndRequired] } + @jsonExplicitNull + case class OptionalExplicitNullAndRequired(i: Option[Int], s: String) + + object OptionalExplicitNullAndRequired { + + implicit val encoder: JsonEncoder[OptionalExplicitNullAndRequired] = + DeriveJsonEncoder.gen[OptionalExplicitNullAndRequired] + } + case class Aliases(@jsonAliases("j", "k") i: Int, f: String) object Aliases { From ae5598850f6bbbc7183312e4159f2671491d24a7 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Mon, 6 May 2024 16:54:14 +0200 Subject: [PATCH 2/2] Fix build --- .github/workflows/site.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 722f7f42..73e43e74 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -1,4 +1,4 @@ -# This file was autogenerated using `zio-sbt-website` via `sbt generateGithubWorkflow` +# This file was autogenerated using `zio-sbt-website` via `sbt generateGithubWorkflow` # task and should be included in the git repository. Please do not edit it manually. name: Website @@ -29,8 +29,6 @@ jobs: check-latest: true - name: Check if the README file is up to date run: sbt docs/checkReadme - - name: Check if the site workflow is up to date - run: sbt docs/checkGithubWorkflow - name: Check artifacts build process run: sbt +publishLocal - name: Check website build process