Scala, spray-json: универсальное перечисление форматирования json

У меня есть такая модель: два перечисления и один класс case с двумя полями этих типов перечислений:

// see later, why objects are implicit
implicit object Fruits extends Enumeration {
  val Apple = Value("apple")
  val Orange = Value("orange")
}

implicit object Vegetables extends Enumeration {
  val Potato = Value("potato")
  val Cucumber = Value("cucumber")
  val Tomato = Value("tomato")
}

type Fruit = Fruits.Value
type Vegetable = Vegetables.Value

case class Pair(fruit: Fruit, vegetable: Vegetable)

Я хочу анализировать/генерировать JSON в/из пар с помощью spray-json. Я не хочу объявлять отдельные JsonFormat для фруктов и овощей. Итак, я хотел бы сделать что-то вроде этого:

import spray.json._
import spray.json.DefaultJsonProtocol._

// enum is implicit here, that's why we needed implicit objects
implicit def enumFormat[A <: Enumeration](implicit enum: A): RootJsonFormat[enum.Value] =
  new RootJsonFormat[enum.Value] {
    def read(value: JsValue): enum.Value = value match {
      case JsString(s) =>
        enum.withName(s)
      case x =>
        deserializationError("Expected JsString, but got " + x)
    }

    def write(obj: enum.Value) = JsString(obj.toString)
  }

// compilation error: couldn't find implicits for JF[Fruit] and JF[Vegetable]
implicit val pairFormat = jsonFormat2(Pair)

// expected value:
// spray.json.JsValue = {"fruit":"apple","vegetable":"potato"}
// but actually doesn't even compile
Pair(Fruits.Apple, Vegetables.Potato).toJson

К сожалению, enumFormat не создает неявных значений для jsonFormat2. Если я пишу вручную два неявных объявления перед форматом парных форматов фруктов и овощей, то маршаллинг json работает:

implicit val fruitFormat: RootJsonFormat[Fruit] = enumFormat(Fruits)
implicit val vegetableFormat: RootJsonFormat[Vegetable] = enumFormat(Vegetables)

implicit val pairFormat = jsonFormat2(Pair)

// {"fruit":"apple","vegetable":"potato"}, as expected
Pair(Fruits.Apple, Vegetables.Potato).toJson

Итак, два вопроса:

  1. Как избавиться от этих объявлений fruitFormat и vegetableFormat?

  2. В идеале было бы здорово не делать объекты перечисления неявными, сохраняя при этом общую функцию enumFormat. Есть ли способ добиться этого? Возможно, используя пакет scala.reflect или что-то в этом роде.


person Evgeny Veretennikov    schedule 06.10.2017    source источник


Ответы (2)


Вам просто нужно заменить enum.Value на A#Value.

Глядя на spray-json #200, вы можете найти пример хорошо определенного неявного enumFormat, слегка измененный для использования неявного enu поиска:

implicit def enumFormat[T <: Enumeration](implicit enu: T): RootJsonFormat[T#Value] =
  new RootJsonFormat[T#Value] {
    def write(obj: T#Value): JsValue = JsString(obj.toString)
    def read(json: JsValue): T#Value = {
      json match {
        case JsString(txt) => enu.withName(txt)
        case somethingElse => throw DeserializationException(s"Expected a value from enum $enu instead of $somethingElse")
      }
    }
}
person Federico Pellegatta    schedule 10.10.2017

Вы не можете, Scala не допускает неявных цепочек, потому что это привело бы к комбинаторным взрывам, которые сделали бы компилятор слишком медленным.

См. https://docs.scala-lang.org/tutorials/FAQ/chaining-implicits.html

Однако Scala не допускает двух таких неявных преобразований, поэтому невозможно перейти от A к C, используя неявное преобразование A в B и другое неявное преобразование B в C.

Вам нужно будет явно создать JsonFormat[T] для каждого T, который вы хотите использовать.

person Rich    schedule 09.10.2017