Search code examples
scalagenericsserializationtraitscompanion-object

Create a companion object that mixes in a trait that defines a method which returns an object of the object's companion class


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?


Solution

  • 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.