Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ways to configure the explicit encoding of empty options as null (#1085) #1100

Merged
merged 2 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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" % "<version>"
libraryDependencies += "dev.zio" %% "zio-json" % "0.6.2"
```

## Example
Expand Down
16 changes: 12 additions & 4 deletions zio-json/shared/src/main/scala-2.x/zio/json/macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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(",")
Expand Down
11 changes: 9 additions & 2 deletions zio-json/shared/src/main/scala-3/zio/json/macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")(
Expand Down Expand Up @@ -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
)
}
)
)
}
10 changes: 10 additions & 0 deletions zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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 {
Expand Down
Loading