From 2e27609cda0f8db1aadc8ab18786a1a5b466a3b7 Mon Sep 17 00:00:00 2001 From: Igor Shymko Date: Fri, 2 Dec 2016 14:24:41 +0200 Subject: [PATCH] Serialize/deserialize maps having numerical keys. Fix #125 --- .../scala/spray/json/CollectionFormats.scala | 24 ++++++++++++++++++- .../spray/json/CollectionFormatsSpec.scala | 19 ++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/main/scala/spray/json/CollectionFormats.scala b/src/main/scala/spray/json/CollectionFormats.scala index 24ab1dd0..36e13285 100644 --- a/src/main/scala/spray/json/CollectionFormats.scala +++ b/src/main/scala/spray/json/CollectionFormats.scala @@ -17,6 +17,8 @@ package spray.json +import scala.util.{Try, Success, Failure} + trait CollectionFormats { /** @@ -62,6 +64,26 @@ trait CollectionFormats { } } + implicit def mapAnyValKeyFormat[K <: AnyVal : JsonFormat, V : JsonFormat] = new RootJsonFormat[Map[K, V]] { + def write(m: Map[K, V]) = JsObject { + m.map { field => + field._1.toJson match { + case JsNumber(x) if x.isValidLong => x.toString -> field._2.toJson + case x => throw new SerializationException("Map key must be convertible to JsNumber, not '" + x + "'") + } + } + } + def read(value: JsValue) = value match { + case x: JsObject => x.fields.map { field => + Try(JsNumber(BigDecimal(field._1))) match { + case Success(k) => (k.convertTo[K], field._2.convertTo[V]) + case Failure(_) => deserializationError("Expected Map key to be deserializable to JsNumber, but got '" + field._1 + "'") + } + } (collection.breakOut) + case x => deserializationError("Expected Map as JsObject, but got " + x) + } + } + import collection.{immutable => imm} implicit def immIterableFormat[T :JsonFormat] = viaSeq[imm.Iterable[T], T](seq => imm.Iterable(seq :_*)) @@ -90,4 +112,4 @@ trait CollectionFormats { case x => deserializationError("Expected Collection as JsArray, but got " + x) } } -} \ No newline at end of file +} diff --git a/src/test/scala/spray/json/CollectionFormatsSpec.scala b/src/test/scala/spray/json/CollectionFormatsSpec.scala index 3d953bb5..2eb1e5ba 100644 --- a/src/test/scala/spray/json/CollectionFormatsSpec.scala +++ b/src/test/scala/spray/json/CollectionFormatsSpec.scala @@ -52,8 +52,21 @@ 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" in { + Map(1 -> "a").toJson mustEqual JsObject("1" -> JsString("a")) + } + "be able to convert a JsObject to a Map[Long, Int]" in { + val jsn = JsObject("1" -> JsNumber(1), "2" -> JsNumber(2), "3" -> JsNumber(3)) + val mp: Map[Long, Int] = Map(1L -> 1, 2L -> 2, 3L -> 3) + + jsn.convertTo[Map[Long, Int]] mustEqual Map(1L -> 1, 2L -> 2, 3L -> 3) + jsn.convertTo[Map[Int, Long]] mustEqual Map(1 -> 1L, 2 -> 2L, 3 -> 3L) + } + "throw an Exception when trying to deserialize a map whose key are not deserialized to JsNumber" in { + JsObject("a" -> JsString("1")).convertTo[Map[Int, String]] must throwA(new DeserializationException("Expected Map key to be deserializable to JsNumber, but got 'a'")) + } + "throw an Exception when trying to serialize a map whose key are not serialized to JsNumber" in { + Map(1.5 -> "a").toJson must throwA(new SerializationException("Map key must be convertible to JsNumber, not '1.5'")) } } @@ -79,4 +92,4 @@ class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { } } -} \ No newline at end of file +}