diff --git a/src/main/scala/spray/json/CollectionFormats.scala b/src/main/scala/spray/json/CollectionFormats.scala index ef94297b..1961662e 100644 --- a/src/main/scala/spray/json/CollectionFormats.scala +++ b/src/main/scala/spray/json/CollectionFormats.scala @@ -44,10 +44,9 @@ 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]] { + 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 { @@ -64,6 +63,23 @@ trait CollectionFormats { } } + /** + * 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 + } + } + def read(value: JsValue) = value match { + case x: JsObject => x.fields.map { field => + (JsString(field._1).fromKey, field._2.convertTo[V]) + } + 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 :_*)) 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 } }