From d17fca5be28c5591198831bdf6eea28f1d7ed38d Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 19 May 2020 21:52:48 +0200 Subject: [PATCH 1/2] Add KeyableFormat for Map json serialization (#125). --- .../scala/spray/json/CollectionFormats.scala | 12 ++++------ .../spray/json/DefaultJsonProtocol.scala | 1 + src/main/scala/spray/json/KeyableFormat.scala | 11 +++++++++ .../scala/spray/json/KeyableFormats.scala | 9 ++++++++ src/main/scala/spray/json/package.scala | 6 +++++ .../spray/json/CollectionFormatsSpec.scala | 23 +++++++++++++++++-- 6 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 src/main/scala/spray/json/KeyableFormat.scala create mode 100644 src/main/scala/spray/json/KeyableFormats.scala diff --git a/src/main/scala/spray/json/CollectionFormats.scala b/src/main/scala/spray/json/CollectionFormats.scala index ef94297b..b49b192a 100644 --- a/src/main/scala/spray/json/CollectionFormats.scala +++ b/src/main/scala/spray/json/CollectionFormats.scala @@ -44,21 +44,17 @@ trait CollectionFormats { } /** - * Supplies the JsonFormat for Maps. The implicitly available JsonFormat for the key type K must - * always write JsStrings, otherwise a [[spray.json.SerializationException]] will be thrown. + * Supplies the JsonFormat for Maps. */ - implicit def mapFormat[K :JsonFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { + implicit def mapFormat[K :KeyableFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { def write(m: Map[K, V]) = JsObject { m.map { field => - field._1.toJson match { - case JsString(x) => x -> field._2.toJson - case x => throw new SerializationException("Map key must be formatted as JsString, not '" + x + "'") - } + field._1.toKey -> field._2.toJson } } def read(value: JsValue) = value match { case x: JsObject => x.fields.map { field => - (JsString(field._1).convertTo[K], field._2.convertTo[V]) + (JsString(field._1).fromKey, field._2.convertTo[V]) } case x => deserializationError("Expected Map as JsObject, but got " + x) } diff --git a/src/main/scala/spray/json/DefaultJsonProtocol.scala b/src/main/scala/spray/json/DefaultJsonProtocol.scala index 4c931845..c4598938 100644 --- a/src/main/scala/spray/json/DefaultJsonProtocol.scala +++ b/src/main/scala/spray/json/DefaultJsonProtocol.scala @@ -26,5 +26,6 @@ trait DefaultJsonProtocol with CollectionFormats with ProductFormats with AdditionalFormats + with KeyableFormats object DefaultJsonProtocol extends DefaultJsonProtocol diff --git a/src/main/scala/spray/json/KeyableFormat.scala b/src/main/scala/spray/json/KeyableFormat.scala new file mode 100644 index 00000000..fff3b7ef --- /dev/null +++ b/src/main/scala/spray/json/KeyableFormat.scala @@ -0,0 +1,11 @@ +package spray.json + +trait KeyableWriter[T] { + def write(obj: T): String +} + +trait KeyableReader[T] { + def read(jsString: JsString): T +} + +trait KeyableFormat[T] extends KeyableWriter[T] with KeyableReader[T] diff --git a/src/main/scala/spray/json/KeyableFormats.scala b/src/main/scala/spray/json/KeyableFormats.scala new file mode 100644 index 00000000..acc43461 --- /dev/null +++ b/src/main/scala/spray/json/KeyableFormats.scala @@ -0,0 +1,9 @@ +package spray.json + +trait KeyableFormats { + + implicit object StringKeyableFormat extends KeyableFormat[String] { + override def write(obj: String) = obj + override def read(json: JsString) = json.value + } +} diff --git a/src/main/scala/spray/json/package.scala b/src/main/scala/spray/json/package.scala index a8a42f05..05984c84 100644 --- a/src/main/scala/spray/json/package.scala +++ b/src/main/scala/spray/json/package.scala @@ -28,6 +28,7 @@ package object json { implicit def enrichAny[T](any: T) = new RichAny(any) implicit def enrichString(string: String) = new RichString(string) + implicit def enrichJsString[T](jsString: JsString) = new RichJsString(jsString) @deprecated("use enrichAny", "1.3.4") def pimpAny[T](any: T) = new PimpedAny(any) @@ -42,6 +43,7 @@ package json { private[json] class RichAny[T](any: T) { def toJson(implicit writer: JsonWriter[T]): JsValue = writer.write(any) + def toKey(implicit writer: KeyableWriter[T]): String = writer.write(any) } private[json] class RichString(string: String) { @@ -51,6 +53,10 @@ package json { def parseJson(settings: JsonParserSettings): JsValue = JsonParser(string, settings) } + private[json] class RichJsString(jsString: JsString) { + def fromKey[T](implicit reader: KeyableReader[T]): T = reader.read(jsString) + } + @deprecated("use RichAny", "1.3.4") private[json] class PimpedAny[T](any: T) { def toJson(implicit writer: JsonWriter[T]): JsValue = writer.write(any) diff --git a/src/test/scala/spray/json/CollectionFormatsSpec.scala b/src/test/scala/spray/json/CollectionFormatsSpec.scala index 9d6970bf..f843cd2b 100644 --- a/src/test/scala/spray/json/CollectionFormatsSpec.scala +++ b/src/test/scala/spray/json/CollectionFormatsSpec.scala @@ -52,8 +52,27 @@ class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { "be able to convert a JsObject to a Map[String, Long]" in { json.convertTo[Map[String, Long]] mustEqual map } - "throw an Exception when trying to serialize a map whose key are not serialized to JsStrings" in { - Map(1 -> "a").toJson must throwA(new SerializationException("Map key must be formatted as JsString, not '1'")) + "convert a Map[Int, String] to a JsObject given a KeyableFormat" in { + implicit val keyableFormat: KeyableFormat[Int] = new KeyableFormat[Int] { + override def read(jsString: JsString) = jsString.value.toInt + override def write(obj: Int) = obj.toString + } + + val map = Map(1 -> "a", 2 -> "b", 3 -> "c") + val json = JsObject("1" -> JsString("a"), "2" -> JsString("b"), "3" -> JsString("c")) + + map.toJson mustEqual json + } + "be able to convert a JsObject to a Map[Int, String] given a KeyableFormat" in { + implicit val keyableFormat: KeyableFormat[Int] = new KeyableFormat[Int] { + override def read(jsString: JsString) = jsString.value.toInt + override def write(obj: Int) = obj.toString + } + + val map = Map(1 -> "a", 2 -> "b", 3 -> "c") + val json = JsObject("1" -> JsString("a"), "2" -> JsString("b"), "3" -> JsString("c")) + + json.convertTo[Map[Int, String]] mustEqual map } } From 61a49cdff24d0b0cd7555ea781e6075fc007d013 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 8 Mar 2021 22:04:32 +0100 Subject: [PATCH 2/2] Reverting existing signature, dropping implicit status. --- .../scala/spray/json/CollectionFormats.scala | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/scala/spray/json/CollectionFormats.scala b/src/main/scala/spray/json/CollectionFormats.scala index b49b192a..1961662e 100644 --- a/src/main/scala/spray/json/CollectionFormats.scala +++ b/src/main/scala/spray/json/CollectionFormats.scala @@ -46,7 +46,27 @@ trait CollectionFormats { /** * Supplies the JsonFormat for Maps. */ - implicit def mapFormat[K :KeyableFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { + def mapFormat[K :JsonFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { + def write(m: Map[K, V]) = JsObject { + m.map { field => + field._1.toJson match { + case JsString(x) => x -> field._2.toJson + case x => throw new SerializationException("Map key must be formatted as JsString, not '" + x + "'") + } + } + } + def read(value: JsValue) = value match { + case x: JsObject => x.fields.map { field => + (JsString(field._1).convertTo[K], field._2.convertTo[V]) + } + case x => deserializationError("Expected Map as JsObject, but got " + x) + } + } + + /** + * Supplies the JsonFormat for Maps. + */ + implicit def keyableMapFormat[K :KeyableFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { def write(m: Map[K, V]) = JsObject { m.map { field => field._1.toKey -> field._2.toJson