Search code examples
jsonscalaspray-json

Json deserialization of Scala case objects with spray-json


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?


Solution

  • 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 :-) )