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

How to emit null instead of nothing when decoding Option[A]? #638

Closed
beneyal opened this issue Jun 15, 2022 · 2 comments
Closed

How to emit null instead of nothing when decoding Option[A]? #638

beneyal opened this issue Jun 15, 2022 · 2 comments

Comments

@beneyal
Copy link

beneyal commented Jun 15, 2022

Hello everyone! 👋

First, thank you for a wonderful library and the hard work you put into it 🙂

Continuing #238, I'm not sure how to use a newtype in order to emit null instead of nothing when decoding Option[A].

I thought about doing something like this:

class Nullable[+A](val oa: Option[A]) extends AnyVal

And then implementing an implicit def (like JsonDecoder.option), but that didn't compile, and I'm not even sure delegating to the existing option decoder is the right direction.

What is the correct way of going about this?

Any help is much appreciated ❤️

@alphaho
Copy link

alphaho commented Jul 21, 2022

@beneyal , here's what is a snippet I managed to make it work. hope that helps!

import zio.json._
import zio.json.ast._
import zio.json.internal.Write

case class OptionAsJsonNull[+A](value: Option[A]) extends AnyVal

object OptionAsJsonNull {
  // optional implicit conversion methods to make it easier to work with
  implicit def toOption[A](value: OptionAsJsonNull[A]): Option[A]   = value.value
  implicit def fromOption[A](value: Option[A]): OptionAsJsonNull[A] = OptionAsJsonNull(value)

  private def decoder[A](implicit A: JsonDecoder[A]): JsonDecoder[OptionAsJsonNull[A]] =
    JsonDecoder[Option[A]].map(OptionAsJsonNull(_))

  // modified on top of JsonEncoder.option
  private def encoder[A](implicit A: JsonEncoder[A]): JsonEncoder[OptionAsJsonNull[A]] = new JsonEncoder[Option[A]] {

    def unsafeEncode(oa: Option[A], indent: Option[Int], out: Write): Unit = oa match {
      case None    => out.write("null")
      case Some(a) => A.unsafeEncode(a, indent, out)
    }

    override def isNothing(oa: Option[A]): Boolean =
      oa match {
        case None    => false
        case Some(a) => A.isNothing(a)
      }

    override final def toJsonAST(oa: Option[A]): Either[String, Json] =
      oa match {
        case None    => Right(Json.Null)
        case Some(a) => A.toJsonAST(a)
      }
  }.contramap(_.value)

  implicit def codec[A](implicit A: JsonCodec[A]): JsonCodec[OptionAsJsonNull[A]] =
    JsonCodec(encoder(JsonEncoder[A]), decoder(JsonDecoder[A]))
}

And the correspondent tests:

import zio.json._
import zio.test._

object OptionAsJsonNullTest extends ZIOSpecDefault {
  override def spec: Spec[TestEnvironment, Any] = suite("OptionAsJsonNullTest")(
    test("property should be emitted as null") {
      assertTrue(WillBeNullProperty(None).toJson == """{"property":null}""")
    },
    test("property should be encoded as nothing") {
      assertTrue(WillBeEmptyBody(None).toJson == """{}""")
    }
  )

  case class WillBeEmptyBody(property: Option[Int])
  object WillBeEmptyBody {
    implicit val jsonCodec: JsonCodec[WillBeEmptyBody] = DeriveJsonCodec.gen
  }
  case class WillBeNullProperty(property: OptionAsJsonNull[Int])
  object WillBeNullProperty {
    implicit val jsonCodec: JsonCodec[WillBeNullProperty] = DeriveJsonCodec.gen
  }
}

@beneyal
Copy link
Author

beneyal commented Jul 28, 2022

Thank you very much @alphaho!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants