Воспроизвести средство форматирования JSON для карты [Int, _]

Я пытаюсь перенести приложение Rails / Mongodb в Play 2.3, используя расширения play-reactivemongo и reactivemongo. При моделировании своих данных я сталкиваюсь с проблемой сериализации и десериализации карты [Int, Boolean].

Когда я пытаюсь определить свои форматы с помощью макроса, например,

implicit val myCaseClass = Json.format[MyCaseClass]

где MyCaseClass имеет несколько строковых полей, поле BSONObjectID и поле Map [Int, Boolean], на которое компилятор жалуется:

No Json serializer found for type Map[Int,Boolean]. Try to implement an implicit Writes or Format for this type.
No Json deserializer found for type Map[Int,Boolean]. Try to implement an implicit Reads or Format for this type.

Глядя на исходный код для Play в Reads.scala, я вижу чтение, определенное для Map [String, _], но не для Map [Int, _].

Есть ли причина, по которой Play по умолчанию имеет чтение / запись для строковых карт, но не для других простых типов?

Я не совсем понимаю карту [String, _], определенную play, потому что я новичок в scala. Как мне перевести это на карту [Int, _]? Если это невозможно по какой-либо технической причине, как мне определить чтение / запись для карты [Int, Boolean]?


person imagio    schedule 04.12.2014    source источник


Ответы (8)


вы можете писать свои собственные чтения и записи в процессе игры.

в вашем случае это будет выглядеть так:

implicit val mapReads: Reads[Map[Int, Boolean]] = new Reads[Map[Int, Boolean]] {
    def reads(jv: JsValue): JsResult[Map[Int, Boolean]] =
        JsSuccess(jv.as[Map[String, Boolean]].map{case (k, v) =>
            Integer.parseInt(k) -> v .asInstanceOf[Boolean]
        })
}

implicit val mapWrites: Writes[Map[Int, Boolean]] = new Writes[Map[Int, Boolean]] {
    def writes(map: Map[Int, Boolean]): JsValue =
        Json.obj(map.map{case (s, o) =>
            val ret: (String, JsValueWrapper) = s.toString -> JsBoolean(o)
            ret
        }.toSeq:_*)
}

implicit val mapFormat: Format[Map[Int, Boolean]] = Format(mapReads, mapWrites)

Тестировал на play 2.3. Я не уверен, что лучше всего иметь Map [Int, Boolean] на стороне сервера и объект json со строковым -> логическим отображением на стороне клиента.

person pichsenmeister    schedule 04.12.2014

JSON допускает только строковые ключи (ограничение, унаследованное от JavaScript).

person Seth Tisue    schedule 04.12.2014
comment
Если бы я остановился на мгновение, чтобы подумать, я бы понял, что я уже знал это, ха-ха. Просто на мгновение, когда я веду себя глупо, извини. В любом случае, можно ли определить чтение / запись так, чтобы объект scala имел Map [Int, Boolean], но записывал объект JSON со строковыми ключами? По сути, разбирать целые числа из всех ключей JSON, чтобы создать карту [Int, _]? - person imagio; 04.12.2014
comment
Я предлагаю вам вернуться и улучшить свой вопрос. Надеюсь, тогда ответит кто-нибудь (кто лучше разбирается в Play). - person Seth Tisue; 04.12.2014

Спасибо Сету Тисью. Это мои «дженерики» (наполовину).

«половина», потому что он не обрабатывает общий ключ. можно скопировать пасту и заменить "Long" на "Int"

"Сводка" - это тип, который я хотел сериализовать (и ему нужен был собственный сериализатор).

/** this is how to create reader and writer or format for Maps*/
//  implicit val mapReads: Reads[Map[Long, Summary]] = new MapLongReads[Summary]
//  implicit val mapWrites: Writes[Map[Long, Summary]] = new MapLongWrites[Summary]
implicit val mapLongSummaryFormat: Format[Map[Long, Summary]] = new MapLongFormats[Summary]

Это необходимая реализация:

class MapLongReads[T]()(implicit reads: Reads[T]) extends Reads[Map[Long, T]] {
  def reads(jv: JsValue): JsResult[Map[Long, T]] =
    JsSuccess(jv.as[Map[String, T]].map{case (k, v) =>
      k.toString.toLong -> v .asInstanceOf[T]
    })
}

class MapLongWrites[T]()(implicit writes: Writes[T])  extends Writes[Map[Long, T]] {
  def writes(map: Map[Long, T]): JsValue =
    Json.obj(map.map{case (s, o) =>
      val ret: (String, JsValueWrapper) = s.toString -> Json.toJson(o)
      ret
    }.toSeq:_*)
}

class MapLongFormats[T]()(implicit format: Format[T]) extends Format[Map[Long, T]]{
  override def reads(json: JsValue): JsResult[Map[Long, T]] = new MapLongReads[T].reads(json)
  override def writes(o: Map[Long, T]): JsValue = new MapLongWrites[T].writes(o)
}
person ozma    schedule 08.03.2016

Мы можем обобщить решение 3x14159265 и Seth Tisue с ​​помощью 2 небольших классов типов:

import play.api.libs.json.Json.JsValueWrapper
import play.api.libs.json._
import simulacrum._

object MapFormat {

  @typeclass trait ToString[A] {
    def toStringValue(v: A): String
  }
  @typeclass trait FromString[A] {
    def fromString(v: String): A
  }

  implicit final def mapReads[K: FromString, V: Reads]: Reads[Map[K, V]] = 
    new Reads[Map[K, V]] {
      def reads(js: JsValue): JsResult[Map[K, V]] =
        JsSuccess(js.as[Map[String, V]].map { case (k, v) => FromString[K].fromString(k) -> v })
    }

  implicit final def mapWrites[K: ToString, V: Writes]: Writes[Map[K, V]] = 
    new Writes[Map[K, V]] {
      def writes(map: Map[K, V]): JsValue =
        Json.obj(map.map {
          case (s, o) =>
            val ret: (String, JsValueWrapper) = ToString[K].toStringValue(s) -> o
            ret
        }.toSeq: _*)
    }

  implicit final def mapFormat[K: ToString: FromString, V: Format]: Format[Map[K, V]] = Format(mapReads, mapWrites)

}

Обратите внимание, что я использую Simulacrum (https://github.com/mpilquist/simulacrum) для определения своего типа классы.

Вот пример того, как его использовать:

final case class UserId(value: String) extends AnyVal

object UserId {
  import MapFormat._

  implicit final val userToString: ToString[UserId] = 
    new ToString[UserId] {
      def toStringValue(v: UserId): String = v.value
    }

  implicit final val userFromString: FromString[UserId] = 
    new FromString[UserId] {
      def fromString(v: String): UserId = UserId(v)
    }
}

object MyApp extends App {

  import MapFormat._

  val myMap: Map[UserId, Something] = Map(...)

  Json.toJson(myMap)
}

если IntelliJ сообщает, что ваш import MapFormat._ никогда не используется, вы можете и это: implicitly[Format[Map[UserId, Something]]] чуть ниже импорта. Это исправит pb. ;)

person Jules Ivanic    schedule 12.06.2017
comment
У вас есть небольшая опечатка в имени значения экземпляра FromString для UserId, что мешает его компиляции. - person Bilk; 21.06.2018

Play Json предоставляет встроенные mapReads и mapWrites для чтения и записи карт.

mapReads принимает (String => JsResult[K]), чтобы вы могли преобразовать ключ в свой собственный тип.

mapWrites возвращает Writes[Map[String, Boolean]], и вы можете использовать contramap, чтобы преобразовать этот писатель в тот, который работает с Map[Int, Boolean]

import play.api.libs.json.{JsResult, Reads, Writes}
import scala.util.Try

import play.api.libs.json.Reads.mapReads
import play.api.libs.json.MapWrites.mapWrites

object MapExample {

  implicit val reads: Reads[Map[Int, Boolean]] =
    mapReads[Int, Boolean](s => JsResult.fromTry(Try(s.toInt)))

  implicit val writes: Writes[Map[Int, Boolean]] =
    mapWrites[Boolean].contramap(_.map { case (k, v) => k.toString -> v})
}

person Jake    schedule 21.01.2021

Как и принятый ответ - немного короче:

implicit val mapReads: Reads[Map[Int, Boolean]] = (jv: JsValue) =>
    JsSuccess(jv.as[Map[String, Boolean]].map { case (k, v) =>
      k.toInt -> v
    })

implicit val mapWrites: Writes[Map[Int, Boolean]] = (map: Map[Int, Boolean]) =>
    Json.toJson(map.map { case (s, o) =>
     s.toString -> o
    })

implicit val jsonMapFormat: Format[Map[Int, Boolean]] = Format(mapReads, mapWrites)

Вот небольшой тест:

val json = Json.toJson(Map(1 -> true, 2 -> false))        
println(json) // {"1":true,"2":false}
println(json.validate[Map[Int, Boolean]]) // JsSuccess(Map(1 -> true, 2 -> false),)
person pme    schedule 22.12.2018

Конкретные KeyWrites и KeyReads доступны в play-json 2.9.x

private implicit val longKeyWrites = KeyWrites[Int](_.toString)
private implicit val longKeyReads =
    KeyReads[Int](str => Try(str.toInt).fold(e => JsError(e.getMessage), JsSuccess(_)))

Json.obj("1" -> "test").validate[Map[Int,String]] // JsSuccess(Map(1 -> test))
person Somatik    schedule 31.05.2021

https://gist.github.com/fancellu/0bea53f1a1dda712e179892785572ce3

Вот способ сохранить карту [NotString, ...]

person Dino Fancellu    schedule 17.05.2020