2

I'm not a fan of bringing implicit parameters into my code so where I use them I want to encapsulate their use. So I am trying to define an object that both wraps up calls to spray-json with exception handling and contains default implicit JsonFormats for each of my model classes. However the implicit parameters are not resolved unless they are imported into the client, calling code, which is exactly where I don't want them to be. Here's what I have so far (which doesn't resolve the implicit formatters), is there a way I can get what I want to work?

package com.rsslldnphy.json

import com.rsslldnphy.models._
import spray.json._

object Json extends DefaultJsonProtocol {

  implicit val personFormat = jsonFormat1(Person)
  implicit val animalFormat = jsonFormat1(Animal)

  def parse[T](s:String)(implicit reader: JsonReader[T]): Option[T] = {
    try { Some(JsonParser(s).convertTo[T]) }
    catch { case e: DeserializationException => None }
  }
}

NB. a JsonFormat is a type of JsonReader

EDIT: Here's what I've written based on @paradigmatic's second suggestion (which I can't get to work, I still get Cannot find JsonReader or JsonFormat type class for T). Am I missing something?

object Protocols extends DefaultJsonProtocol {
  implicit val personFormat = jsonFormat1(Person)
  implicit val animalFormat = jsonFormat1(Animal)
}

object Json {

  def parse[T](s:String): Option[T] = {
    import Protocols._
    try { Some(JsonParser(s).convertTo[T]) }
    catch { case e: DeserializationException => None }
  }
}

For the record, this is a code snippet that does work, but that I'm trying to avoid as it requires too much of the client code (ie. it needs to have the implicits in scope):

object Json extends DefaultJsonProtocol {
  implicit val personFormat = jsonFormat1(Person)
  implicit val animalFormat = jsonFormat1(Animal)
}

object ClientCode {
  import Json._
  def person(s: String): Person = JsonParser(s).convertTo[Person]
}
Russell
  • 12,261
  • 4
  • 52
  • 75
  • I'm not sure when you say "encapsulate their use" whether you mean to not bring them in explicitly, or whether you mean to keep the scope in which the implicits are available as narrow as possible. If the former, have you considered using a package object? See e.g. http://stackoverflow.com/questions/12901554/scala-delegate-import-of-implicit-conversions – Steve Waldman Nov 11 '12 at 12:12
  • I want to have an object in which the implicits reside along with the parse method such that I can call the parse method on the object without having any implicits in scope in the client code. – Russell Nov 11 '12 at 13:00
  • 1
    @Russell: I can understand an aversion to having a lot of implicit _conversions_ lying around, but implicit instances are exactly what makes the type class pattern work in Scala. – Travis Brown Nov 11 '12 at 13:39
  • @TravisBrown I am only ever going to have one format per model so I don't want client code to have to know or care about either passing it as an argument or having it in scope. Essentially what I'm asking for is the ability to provide class based defaults. – Russell Nov 11 '12 at 13:42

1 Answers1

7

You could declare the implicits in the companion objects:

object Person {
  implicit val personFormat: JReader[Person] = jsonFormat1(Person)
}
object Animal {
  implicit val animalFormat: JReader[Animal] = jsonFormat1(Animal)
}

The implicit resolution rules are very complex. You can find more information in this blog post. But if the compiler is looking for a typeclass T[A], it will look (soon or later) for it in the companion object of class/trait A.

EDIT: If the issue is only a problem of scope "pollution", you could just introduce some braces. With your code example, you could call the function parse as:

package com.rsslldnphy.json

import com.rsslldnphy.models._
import spray.json._

object Json extends DefaultJsonProtocol {
  implicit val personFormat = jsonFormat1(Person)
  implicit val animalFormat = jsonFormat1(Animal)

  def parse[T](s:String)(implicit reader: JsonReader[T]): Option[T] = {
    try { Some(JsonParser(s).convertTo[T]) }
    catch { case e: DeserializationException => None }
  }
}

object JsonFacade {
    def optParse[T]( s: String ): Option[T] = {
      import Json._
      parse[T]( s )
    }
}

Here the implicits "pollutes" only the optParse method.

paradigmatic
  • 40,153
  • 18
  • 88
  • 147
  • Thanks, will have a go at implementing this (although the `jsonFormat1` method comes from inheriting from `DefaultJsonProtocol`, which might make things a bit more complex). Is it possible to do this without polluting the model objects? I want to keep the models and the json api for them completely separate, with the eventual aim of splitting them out into separate jars. – Russell Nov 11 '12 at 12:03
  • @Russell here is a more specific solution. – paradigmatic Nov 11 '12 at 12:17
  • I still get `Cannot find JsonReader or JsonFormat type class for T` with your second solution. – Russell Nov 11 '12 at 12:57
  • I've updated the question with exactly what I've written based on your second suggestion - am I missing something? – Russell Nov 11 '12 at 13:24
  • @Russell The idea is to let your initial solution as it is, but *surround all imports and calls* inside curly braces. Thus you don't mess with the outer scope. That's why I introduced as an example as distinct method which acts like a *facade* (as in GoF). – paradigmatic Nov 11 '12 at 14:29
  • I understand the concept, but it doesn't work in practice - the json reader isn't resolved. Best guess it's something to do with type erasure? – Russell Nov 11 '12 at 14:47
  • @Russell I don't know `spray-json` but the approach works. I used it a lot actually to avoid implicit clashes. BTW: the implicits are resolved at compile-time, before erasure. – paradigmatic Nov 11 '12 at 15:24
  • Well I can't get it to compile! Could you point out what is different in what I've added to the question that would make it not work? Your help is appreciated! – Russell Nov 11 '12 at 15:37
  • @Russell here is the full pattern. If it doesn't compile, report the error message exact location. – paradigmatic Nov 11 '12 at 15:53
  • `[error] /Users/russell.dunphy/Code/Personal/spray-scratch/src/main/scala/com/rsslldnphy/json/Protocols.scala:36: Cannot find JsonReader or JsonFormat type class for T [error] parse[T]( s ) [error] ^ [error] one error found` – Russell Nov 11 '12 at 18:50
  • Line 36 is the line of JsonFacade that reads `parse[T]( s )`. – Russell Nov 11 '12 at 18:51
  • With your initial code snippet (in the question). If you compile it and call `import Json._; parse[...](...)` does it work ? If it doesn't the problem may lie in the `jsonFormat1` function. – paradigmatic Nov 11 '12 at 18:59
  • No, the initial code snippet doesn't work, that was the whole reason for the question! I'll add a snippet that does work. – Russell Nov 11 '12 at 19:44
  • The crux of the problem seems to be that if the type is specified, ie, `parse[Person]`, it works, but if it's generic, ie, `parse[T]`, it doesn't. – Russell Nov 11 '12 at 22:30