2
votes

Is there any way of pattern matching objects where the objects may be Set[Foo] or Set[Bar] when the matching object can be any Object.

Given the below code, trying to pattern match on Set[Bar] will result in a match of Set[Foo] because of type erasure.

import play.api.libs.json._

import scala.collection.immutable.HashMap

case class Foo(valOne: Int, valTwo: Double)

object Foo {
  implicit val writesFoo = Json.writes[Foo]
}

case class Bar(valOne: String)

object Bar {
  implicit val writesBar = Json.writes[Bar]
}

case class TestRequest(params: Map[String, Object])

object TestRequest {

  import play.api.libs.json.Json.JsValueWrapper

  implicit val writeAnyMapFormat = new Writes[Map[String, Object]] {

  def writes(map: Map[String, Object]): JsValue = {
    Json.obj(map.map {
    case (s, a) => {
      val ret: (String, JsValueWrapper) = a match {
        case _: String => s -> JsString(a.asInstanceOf[String])
        case _: java.util.Date => s -> JsString(a.asInstanceOf[String])
        case _: Integer => s -> JsString(a.toString)
        case _: java.lang.Double => s -> JsString(a.toString)
        case None => s -> JsNull
        case foo: Set[Foo] => s -> Json.toJson(a.asInstanceOf[Set[Foo]])
        case bar: Set[Bar] => s -> Json.toJson(a.asInstanceOf[Set[Bar]])
        case str: Set[String] => s -> Json.toJson(a.asInstanceOf[Set[String]])
      }
      ret
    }}.toSeq: _*)
    }
  }

  implicit val writesTestRequest = Json.writes[TestRequest]
}

object MakeTestRequest extends App {
  val params = HashMap[String, Object]("name" -> "NAME", "fooSet" -> Set(Foo(1, 2.0)), "barSet" -> Set(Bar("val1")))

  val testRequest = new TestRequest(params)

  println(Json.toJson(testRequest))

}

Trying to serialise the TestRequest will result in:

Exception in thread "main" java.lang.ClassCastException: Bar cannot be cast to Foo

Delegating the pattern matching of Sets to another method in an attempt to get the TypeTag,

        case _ => s -> matchSet(a)

results in the type, unsurprisingly, of Object.

def matchSet[A: TypeTag](set: A): JsValue = typeOf[A] match {
    case fooSet: Set[Foo] if typeOf[A] =:= typeOf[Foo] => Json.toJson(set.asInstanceOf[Set[Foo]])
    case barSet: Set[Bar] if typeOf[A] =:= typeOf[Bar] => Json.toJson(set.asInstanceOf[Set[Bar]])
}

The runtime error being:

Exception in thread "main" scala.MatchError: java.lang.Object (of class scala.reflect.internal.Types$ClassNoArgsTypeRef)

A workaround could be to check the instance of the first element in the Set but this seems inefficient and ugly. Could also match on the key eg fooSet or barSet but if the keys are the same name eg both called set, then this wouldn't work.

In 2.11 s there any way to get at the type/class the Set has been created with?

1

1 Answers

0
votes

You could use Shapeless typeable. Note that this is still not 100% safe (e.g. empty lists of different types cannot be distinguished at runtime, because that information literally doesn't exist); under the hood it's doing things like checking the types of the elements using reflection, just with a nicer interface on top.

In general it's better to carry the type information around explicitly, e.g. by using a a case class (or a shapeless HMap) rather than an untyped Map. Less good, but still better than nothing, is using wrapper case classes for the different types of Set that are possible, so that each one is a different type at runtime.

(Also half the point of the pattern match is to avoid the asInstanceOf; you should use e.g. foo rather than a.asInstanceOf[Set[Foo]])