From 1a3e5f05d6887e7f849b80a44f193ec1d46c7849 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Mon, 4 Sep 2023 09:44:41 +0200 Subject: [PATCH 1/3] Failing test demonstrating issue --- .../scala/io/sphere/json/OptionReaderSpec.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala index c14b5bdc..defefbd2 100644 --- a/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -1,6 +1,7 @@ package io.sphere.json import io.sphere.json.generic._ +import org.json4s.{JLong, JObject, JString} import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -19,6 +20,10 @@ object OptionReaderSpec { implicit val json: JSON[ComplexClass] = jsonProduct(apply _) } + case class MapClass(id: Long, map: Option[Map[String, String]]) + object MapClass { + implicit val json: JSON[MapClass] = jsonProduct(apply _) + } } class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { @@ -61,6 +66,17 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { result mustEqual None } + "handle optional map" in { + getFromJValue[MapClass](JObject("id" -> JLong(1L))) mustEqual MapClass(1L, None) + + getFromJValue[MapClass](JObject("id" -> JLong(1L), "map" -> JObject())) mustEqual + MapClass(1L, Some(Map.empty)) + + getFromJValue[MapClass]( + JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b")))) mustEqual + MapClass(1L, Some(Map("a" -> "b"))) + } + "handle absence of all fields mixed with ignored fields" in { val json = """{ "value3": "a" }""" val result = getFromJSON[Option[SimpleClass]](json) From 1b0daf8a0feae59b285b07281da824fc8b30f0dd Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Mon, 11 Sep 2023 09:42:50 +0200 Subject: [PATCH 2/3] Added specialized reader for Option[Map[String, A]] --- .../src/main/scala/io/sphere/json/FromJSON.scala | 11 +++++++++++ .../test/scala/io/sphere/json/OptionReaderSpec.scala | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala index b44c751c..9970f5d8 100644 --- a/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala @@ -39,6 +39,17 @@ object FromJSON extends FromJSONInstances { private def validEmptyVector[A]: Valid[Vector[A]] = validEmptyAnyVector.asInstanceOf[Valid[Vector[A]]] + implicit def optionMapReader[@specialized A](implicit + c: FromJSON[A]): FromJSON[Option[Map[String, A]]] = + new FromJSON[Option[Map[String, A]]] { + private val internalMapReader = mapReader[A] + + def read(jval: JValue): JValidation[Option[Map[String, A]]] = jval match { + case JNothing | JNull => validNone + case x => internalMapReader.read(x).map(Some.apply) + } + } + implicit def optionReader[@specialized A](implicit c: FromJSON[A]): FromJSON[Option[A]] = new FromJSON[Option[A]] { def read(jval: JValue): JValidation[Option[A]] = jval match { diff --git a/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala index defefbd2..63a476ba 100644 --- a/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -1,7 +1,7 @@ package io.sphere.json import io.sphere.json.generic._ -import org.json4s.{JLong, JObject, JString} +import org.json4s.{JLong, JNothing, JObject, JString} import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -75,6 +75,13 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { getFromJValue[MapClass]( JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b")))) mustEqual MapClass(1L, Some(Map("a" -> "b"))) + + toJValue[MapClass](MapClass(1L, None)) mustEqual + JObject("id" -> JLong(1L), "map" -> JNothing) + toJValue[MapClass](MapClass(1L, Some(Map()))) mustEqual + JObject("id" -> JLong(1L), "map" -> JObject()) + toJValue[MapClass](MapClass(1L, Some(Map("a" -> "b")))) mustEqual + JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) } "handle absence of all fields mixed with ignored fields" in { From 965f752f826fe3fa0f8f62b10ff8f37856b27c61 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Mon, 11 Sep 2023 10:01:58 +0200 Subject: [PATCH 3/3] Added test for optional list --- .../io/sphere/json/OptionReaderSpec.scala | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala index 63a476ba..affb805e 100644 --- a/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala +++ b/json/json-derivation/src/test/scala/io/sphere/json/OptionReaderSpec.scala @@ -1,7 +1,7 @@ package io.sphere.json import io.sphere.json.generic._ -import org.json4s.{JLong, JNothing, JObject, JString} +import org.json4s.{JArray, JLong, JNothing, JObject, JString} import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -24,6 +24,11 @@ object OptionReaderSpec { object MapClass { implicit val json: JSON[MapClass] = jsonProduct(apply _) } + + case class ListClass(id: Long, list: Option[List[String]]) + object ListClass { + implicit val json: JSON[ListClass] = jsonProduct(apply _) + } } class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { @@ -84,6 +89,24 @@ class OptionReaderSpec extends AnyWordSpec with Matchers with OptionValues { JObject("id" -> JLong(1L), "map" -> JObject("a" -> JString("b"))) } + "handle optional list" in { + getFromJValue[ListClass]( + JObject("id" -> JLong(1L), "list" -> JArray(List(JString("hi"))))) mustEqual + ListClass(1L, Some(List("hi"))) + getFromJValue[ListClass](JObject("id" -> JLong(1L), "list" -> JArray(List.empty))) mustEqual + ListClass(1L, Some(List())) + getFromJValue[ListClass](JObject("id" -> JLong(1L))) mustEqual + ListClass(1L, None) + + toJValue(ListClass(1L, Some(List("hi")))) mustEqual JObject( + "id" -> JLong(1L), + "list" -> JArray(List(JString("hi")))) + toJValue(ListClass(1L, Some(List.empty))) mustEqual JObject( + "id" -> JLong(1L), + "list" -> JArray(List.empty)) + toJValue(ListClass(1L, None)) mustEqual JObject("id" -> JLong(1L), "list" -> JNothing) + } + "handle absence of all fields mixed with ignored fields" in { val json = """{ "value3": "a" }""" val result = getFromJSON[Option[SimpleClass]](json)