0

I am trying to write a custom JsonReader using spray-json for the following domain model:

sealed trait OrderType
object OrderType {
  case object MARKET extends OrderType
  case object LIMIT extends OrderType
  case object STOP extends OrderType
  case object MARKET_IF_TOUCHED extends OrderType
  case object TAKE_PROFIT extends OrderType
  case object STOP_LOSS extends OrderType
  case object TRAILING_STOP_LOSS extends OrderType
}

Here is the custom JsonReader I created for this purpose:

implicit object OrderTypeJsonReader extends JsonReader[OrderType] {
  def read(value: JsValue): OrderType = value match {
    case JsString("MARKET") => MARKET
    case JsString("LIMIT") => LIMIT
    case JsString("STOP") => STOP
    case JsString("MARKET_IF_TOUCHED") => MARKET_IF_TOUCHED
    case JsString("TAKE_PROFIT") => TAKE_PROFIT
    case JsString("STOP_LOSS") => STOP_LOSS
    case JsString("TRAILING_STOP_LOSS") => TRAILING_STOP_LOSS
    case _ => deserializationError("OrderType expected")
  }
}

Given that the json string and the name of the case object are the same, is there any way to avoid code duplication here?

msilb
  • 505
  • 6
  • 18

1 Answers1

0

You could try to replace pattern match with a partial function(or Map):

val orderTypes = List(MARKET, LIMIT, STOP, MARKET_IF_TOUCHED, TAKE_PROFIT, STOP_LOSS, TRAILING_STOP_LOSS)

val string2orderType: Map[JsValue, OrderType] = 
  orderTypes.map(ot => (JsString(ot.toString), ot)).toMap

implicit object OrderTypeJsonReader extends JsonReader[OrderType] {
  def read(value: JsValue): OrderType = 
    string2orderType.getOrElse(value, deserializationError("OrderType expected"))
}

The disadvantage is that you have to specify the list of all case objects manually. You can try to use reflection to generate it. Maybe this question would be helpful for that Getting subclasses of a sealed trait . Then you can have:

import scala.reflect.runtime.universe

private val tpe = universe.typeOf[OrderType]
private val clazz = tpe.typeSymbol.asClass

private def objectBy[T](name: String): T = Class.forName(OrderType.getClass.getName + name + "$").newInstance().asInstanceOf[T]

val string2orderType: Map[JsValue, OrderType] = clazz.knownDirectSubclasses.map { sc =>
  val objectName = sc.toString.stripPrefix("object ")
  (JsString(objectName), objectBy[OrderType](objectName))
}.toMap

implicit object OrderTypeJsonReader extends JsonReader[OrderType] {
  def read(value: JsValue): OrderType = string2orderType.getOrElse(value, deserializationError("OrderType expected"))
}

Please, also see this discussion about adding a default case class format to Spray: https://github.com/spray/spray-json/issues/186

UPDATE to address the comments

Is it possible to 'generify' it for any type T? I have quite a few of those sealed trait / case object enumerations and would prefer to have the boilerplate kept to minimum.

I've come up with this:

import spray.json._
import Utils._

sealed trait OrderStatus
object OrderStatus {
  case object Cancelled extends OrderStatus
  case object Delivered extends OrderStatus
  // More objects...

  implicit object OrderStatusJsonReader extends ObjectJsonReader[OrderStatus]
}

sealed trait OrderType
object OrderType {
  case object MARKET extends OrderType
  case object LIMIT extends OrderType
  // More objects...

  implicit object OrderTypeJsonReader extends ObjectJsonReader[OrderType]
}

object Utils {
  import scala.reflect.ClassTag
  import scala.reflect.runtime.universe._

  def objectBy[T: ClassTag](name: String): T = {
    val c = implicitly[ClassTag[T]]
    Class.forName(c + "$" + name + "$").newInstance().asInstanceOf[T]
  }

  def string2trait[T: TypeTag : ClassTag]: Map[JsValue, T] = {
    val clazz = typeOf[T].typeSymbol.asClass
    clazz.knownDirectSubclasses.map { sc =>
      val objectName = sc.toString.stripPrefix("object ")
      (JsString(objectName), objectBy[T](objectName))
    }.toMap
  }

  class ObjectJsonReader[T: TypeTag : ClassTag] extends JsonReader[T] {
    val string2T: Map[JsValue, T] = string2trait[T]
    def defaultValue: T = deserializationError(s"${ implicitly[ClassTag[T]].runtimeClass.getCanonicalName } expected")
    override def read(json: JsValue): T = string2T.getOrElse(json, defaultValue)
  }
}

Then you can use it like:

import OrderType._
import OrderStatus._
JsString("MARKET").convertTo[OrderType]
JsString(OrderStatus.Cancelled.toString).convertTo[OrderStatus]

I also tried code from spray-json github issue and it can be used like so:

implicit val orderTypeJsonFormat: RootJsonFormat[OrderType] = 
  caseObjectJsonFormat(MARKET, LIMIT, STOP, MARKET_IF_TOUCHED, TAKE_PROFIT, STOP_LOSS, TRAILING_STOP_LOSS)

Unfortunately, this requires you to specify all of the objects explicitly. If you want it like so, then, I think, my first suggestion (without reflection) is better. (Because it is without reflection :-) )

Community
  • 1
  • 1
zhelezoglo
  • 212
  • 2
  • 11
  • Thanks for your answer, I like the reflection code to generate the map from json string to object. Is it possible to 'generify' it for any type `T`? I have quite a few of those sealed trait / case object enumerations and would prefer to have the boilerplate kept to minimum. Btw, I also tried code from [spray-json github issue](https://github.com/spray/spray-json/issues/186) and it can be used like so: `implicit val orderTypeJsonFormat: RootJsonFormat[OrderType] = caseObjectJsonFormat(MARKET, LIMIT, STOP, MARKET_IF_TOUCHED, TAKE_PROFIT, STOP_LOSS, TRAILING_STOP_LOSS)`. – msilb Feb 01 '17 at 14:16
  • @msilb sorry, I couldn't express it concisely, that's why had to update the answer. – zhelezoglo Feb 01 '17 at 16:14
  • Thank you, I tested it and it works fine. In the end I decided to drop spray-json altogether though, mainly because I don't want to have any of this rather fragile "reflection black magic" in my public library, and instead switched to [circe](https://github.com/circe/circe), which seems to do a better job as it relies on [shapeless](https://github.com/milessabin/shapeless) to derive `Decoder`s for `case class`es and `case object`s automatically. Works more or less "out of the box" for my use case. – msilb Feb 06 '17 at 03:57