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 c14b5bdc..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,6 +1,7 @@ package io.sphere.json import io.sphere.json.generic._ +import org.json4s.{JArray, JLong, JNothing, JObject, JString} import org.scalatest.OptionValues import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -19,6 +20,15 @@ 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 _) + } + + 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 { @@ -61,6 +71,42 @@ 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"))) + + 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 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)