0

Abstract problem: Create a trait that can be mixed into the companion object of a class, to give that object a method that returns an object of that class.

Concrete problem: I'm trying to create a bunch of classes for use with RESTful service calls, that know how to serialize and de-serialize themselves, like so:

case class Foo
(
    var bar : String,
    var blip : String
)
extends SerializeToJson
object Foo extends DeserializeFromJson

The intended usage is like so:

var f = Foo( "abc","123" )
var json = f.json
var newF = Foo.fromJson( json )

I'm using Genson to do the serialization/deserialization, which I access through a global object:

object JSON {
  val parser = new ScalaGenson( new GensonBuilder() <...> )
}

Then I define the traits like so:

trait SerializeToJson {
  def json : String = JSON.parser.toJson(this)
}
trait DeserializeFromJson[T <: DeserializeFromJson[T]] {
  def fromJson( json : String ) : T = JSON.parser.fromJson( json )
}

This compiles. But this does not:

object Foo extends DeserializeFromJson[Foo]

I get the following error message:

type arguments [Foo] do not conform to trait DeserializeFromJson's 
type parameter bounds [T <: DeserializeFromJson[T]] 

I've tried creating a single trait, like so:

trait JsonSerialization[T <: JsonSerialization[T]] {

  def json(implicit m: Manifest[JsonSerialization[T]]) : String = 
    JSON.parser.toJson(this)(m)

  def fromJson( json : String ) : T = 
    JSON.parser.fromJson(json)

}

Now, if I just declare case class Foo (...) extends JsonSerialization[Foo] then I can't call Foo.fromJson because only an instance of class Foo has that method, not the companion object.

If I declare object Foo extend JsonSerialization[Foo] then I can compile and Foo has a .fromJson method. But at run time, the call to fromJson thinks that T is a JsonSerialization, and not a Foo, or so the following run-time error suggests:

java.lang.ClassCastException: scala.collection.immutable.HashMap$HashTrieMap cannot be cast to ...JsonSerialization
at ...JsonSerialization$class.fromJson(DataModel.scala:14)
at ...Foo.fromJson(Foo.scala:6)

And I can't declare object Foo extends Foo because I get

module extending its companion class cannot use default constructor arguments

So I can try adding constructor parameters, and that compiles and runs, but again the run-time type when it tries to deserialize is wrong, giving me the above error.

The only thing I've been able to do that works is to define fromJson in every companion object. But there MUST be a way to define it in a trait, and just mix in that trait. Right?

John Arrowwood
  • 2,370
  • 2
  • 21
  • 32

3 Answers3

0

The solution is to simplify the type parameter for the trait.

trait DeserializeFromJson[T] { 
  def fromJson( json : String )(implicit m : Manifest[T]) : T = 
    JSON.parser.fromJson[T](json)(m)
}

Now, the companion object can extend DeserializeFromJson[Foo] and when I call Foo.fromJson( json ) it is able to tell Genson the correct type information so that an object of the appropriate type is created.

John Arrowwood
  • 2,370
  • 2
  • 21
  • 32
0

The problem is related to how implicits work. Genson expects a Manifest that it will use to know to what type it must deserialize. This manifest is defined as implicit in Genson, meaning that it will try to get it from implicitly available manifests in the "caller code". However in your original version there is no Manifest[T] in DeserializeFromJson.

An alternate way would be to define the DeserializeFromJson like that (which will just produce a constructor with an implicit Manifest[T] argument):

abstract class DeserializeFromJson[T: Manifest] {
  def fromJson( json : String ) : T = JSON.parser.fromJson[T](json)
}

object Foo extends DeserializeFromJson[Foo]

More generally if you don't bring more value by encapsulating a lib (in this case Genson), I think you shouldn't do that. As you basically reduce the features of Genson (now people can only work with strings) and introduce problems like the one you hit.

eugen
  • 5,856
  • 2
  • 29
  • 26
  • While I agree with the last comment, I'm not sure of a better approach. The intent is to be able to define a class whose sole purpose is to be used for passing back and forth between an application (in this case, integration tests) and a REST interface. All the class needs to do is store the data in an accessible way (fields), and know how to serialize and deserialize itself. What is the standard way of implementing that? – John Arrowwood Mar 24 '16 at 13:59
0

I think your type parameter constraint were originally wrong; you had

trait DeserializeFromJson[T <: DeserializeFromJson[T]]

With your own answer, you fully relaxed it; you needed

trait DeserializeFromJson[T <: SerializeToJson]

...which the error was trying to tell you.

The need for the implicit Manifest (ClassTag now I believe) or context-bounds was on the money.

Would be nice for Scala to allow the specification of inheritance and type-parameter constraints based on class/trait and companion object relationship, given it is already aware, to some degree, when it comes to access-modifiers and implicit scopes.

Darren Bishop
  • 2,379
  • 23
  • 20