Scala, spray-json: universal enumeration json formatting
Asked Answered
R

2

8

I have such model: two enumerations and one case class with two fields of these enums types:

// 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)

I want to parse/generate JSONs to/from Pairs with spray-json. I don't want to declare separate JsonFormats for fruits and vegetables. So, I'd like to do something like this:

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

Sadly, enumFormat doesn't produce implicit values for jsonFormat2. If I write manually two implicit declarations before pairFormat for fruits and vegetables formats, then json marshalling works:

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

So, two questions:

  1. How to get rid of these fruitFormat and vegetableFormat declarations?

  2. Ideally it would be great to not make enumeration objects implicit, while keeping enumFormat function generic. Is there a way to achieve this? Maybe, using scala.reflect package or something like that.

Refusal answered 6/10, 2017 at 18:52 Comment(0)
T
15

You just have to replace enum.Value with A#Value.

Looking at spray-json #200 you can find an example of a well defined implicit enumFormat, slightly modified to leverage implicit enu retrieval:

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")
      }
    }
}
Thynne answered 10/10, 2017 at 9:24 Comment(0)
V
0

You can't, Scala doesn't allow implicit chaining, because it would lead to combinatorial explosions that would make the compiler too slow.

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

Scala does not allow two such implicit conversions taking place, however, so one cannot got from A to C using an implicit A to B and another implicit B to C.

You'll have to explicitly produce a JsonFormat[T] for each T you want to use.

Vinasse answered 9/10, 2017 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.