Skip to content

Commit

Permalink
Add KeyableFormat for Map json serialization (spray#125).
Browse files Browse the repository at this point in the history
  • Loading branch information
janjaali committed May 19, 2020
1 parent 76fcf7f commit d17fca5
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 10 deletions.
12 changes: 4 additions & 8 deletions src/main/scala/spray/json/CollectionFormats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/spray/json/DefaultJsonProtocol.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ trait DefaultJsonProtocol
with CollectionFormats
with ProductFormats
with AdditionalFormats
with KeyableFormats

object DefaultJsonProtocol extends DefaultJsonProtocol
11 changes: 11 additions & 0 deletions src/main/scala/spray/json/KeyableFormat.scala
Original file line number Diff line number Diff line change
@@ -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]
9 changes: 9 additions & 0 deletions src/main/scala/spray/json/KeyableFormats.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
6 changes: 6 additions & 0 deletions src/main/scala/spray/json/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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)
Expand Down
23 changes: 21 additions & 2 deletions src/test/scala/spray/json/CollectionFormatsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down

0 comments on commit d17fca5

Please sign in to comment.