Question

The spray-json library extends basic Scala types with a toJson method. I'd like to convert an Any into a JsValue if there is such a pimp for the underlying type. My best attempt works, but is verbose:

import cc.spray._

val maybeJson1: PartialFunction[Any, JsValue] = {
  case x: BigDecimal => x.toJson
  case x: BigInt => x.toJson
  case x: Boolean => x.toJson
  case x: Byte => x.toJson
  case x: Char => x.toJson
  case x: Double => x.toJson
  case x: Float => x.toJson
  case x: Int => x.toJson
  case x: Long => x.toJson
  case x: Short => x.toJson
  case x: String => x.toJson
  case x: Symbol => x.toJson
  case x: Unit => x.toJson
}

Ideally, I'd prefer something (impossible) like this:

def maybeJson2(any: Any): Option[JsValue] = {
  if (pimpExistsFor(any))
    Some(any.toJson)
  else
    None  
}

Is there a way to do this without enumerating every type that has been enriched?

Was it helpful?

Solution

There is a way, but it requires a lot of reflection and therefore is quite a headache. The basic idea is as follows. The DefaultJsonProtocol object inherits a bunch of traits that contain implicit objects which contain write methods. Each of those will have an accessor function, but you won't know what it's called. Basically, you'll just take all methods that take no parameters and return one object that has a write method that takes the class of your object and returns a JsValue. If you find exactly one such method that returns one such class, use reflection to call it. Otherwise, bail.

It would look something like this (warning, untested):

def canWriteMe(writer: java.lang.Class[_], me: java.lang.Class[_]): 
  Option[java.lang.reflect.Method] =
{
  writer.getMethods.find(_.getName == "write").filter{ m =>
    classOf[JsValue].isAssignableFrom(m.getReturnType) && {
      val parm = m.getParameterTypes()
      m.length == 1 && parm(0).isAssignableFrom(me)
    }
  }
}
def maybeJson2(any: Any): Option[JsValue] = {
  val couldWork = {
    DefaultJsonProtocol.getClass.getMethods.
      filter(_.getParameterTypes.length==0).
      flatMap(m => canWriteMe(m.getReturnType, any.getClass).map(_ -> m))
  }
  if (couldWork.length != 1) None else {
    couldWork.headOption.map{ case (wrMeth, obMeth) =>
      val wrObj = obMeth.invoke(DefaultJsonProtocol)
      val answer = wrMeth.invoke(wrObj, any)
    }
  }
}

Anyway, you're best off pulling the DefaultJsonProtocol class apart in the REPL step by step and finding out how to reliably identify the objects that define the writers, and then get the write methods out of them.

OTHER TIPS

I'm not sure it will fit you needs, but here is an alternative approach wich is really simple and type-safe.

If you kept the type of the argument (instead of using Any) you could rely on implicit parameter resolution to find the correct conversion at compile time:

def toJson[T:JsonFormat]( t: T ): JsValue = implicitly[JsonFormat[T]].write(t)

You won't need an option, because the program will fail at compile time if you try to pass an argument which is not "pimpable".

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top