Grega Kešpret
Grega Kešpret

Reputation: 12117

Pattern match Jackson JSON in Scala

I am wondering if it is possible to do pattern matching on Jackson JSON objects in Scala. We are currently using jackson-module-scala in a Project heavily and would benefit from being able to do pattern matching of Json ObjectNode/JsonNode objects.

If this is not possible, how would I go about adding this functionality? I was thinking something in terms of implicit conversion from JsonNode/ObjectNode to MyClass, where MyClass would have unapply method, doing JsonNode.toString and regex matching. If my logic is correct, I could then do pattern matching on JsonNode objects. Of course, there could be better ways I am not aware of, or this one may not work for reasons I am not yet aware of. To illustrate my case, I would like to be able to perform something in terms of:

val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)

val json = mapper.createObjectNode()
                .put("key1","value1")
                .put("key2","value2")
                .put("key3","value3")

json match {
  case MyClass("key1", "value1", "key2", y) => println("Found key1 with value1, where key2 is " + y)
  case MyClass("key1", x) => println("Key1 value is " + x)
  ...
  _ => println("No match found")
}

Upvotes: 2

Views: 2337

Answers (1)

drstevens
drstevens

Reputation: 2913

Have you tried to make use of the case class deserialization? https://github.com/FasterXML/jackson-module-scala/blob/master/src/test/scala/com/fasterxml/jackson/module/scala/deser/CaseClassDeserializerTest.scala

If that doesn't work, I think you would be better off creating extractors to represent your domain objects. Code below assumes a scala Map, but it should give you an idea. No implicit conversion required.

case class DomainObjectA(v1: String, v2: String)
object DomainObjectAExtractor {
  def unapply(m: Map[String, String]) = for {
     v1 <- m.get("key1")
     v2 <- m.get("key2")
  } yield DomainObjectA(v1, v2)
}

case class DomainObjectB(v3, v4, v5)
object DomainObjectBExtractor {
  def unapply(m: Map[String, String]) = for {
     v3 <- m.get("key3")
     v4 <- m.get("key4")
     v5 <- m.get("key5")
  } yield DomainObjectB(v3, v4, v5)
}

json match {
  case DomainObjectAExtractor(a@DomainObjectA(_, _)) => a
  case DomainObjectBExtractor(b@DomainObjectB(_, _, _)) => b
}

However, if you insist on trying to match against the key/value pairs, there may be ways to accomplish something which is acceptable for you. It is not possible to pass input into the unapply function from the case, which I think would be required if I understand what you want to do correctly. It may be possible to do this with macros which are experimental in the soon-to-be-officially-released scala 2.10. I haven't played with them enough to know if this is or is not possible though.

If ordering of keys was assumed, you could come up with a :: unapply operator similar to :: for list. This could extract the K, V pairs in this known order. Personally, this is too fragile for my tastes.

val json = Map(("key1" -> "one"), ("key2" -> "two"))

object -> {
  def unapply[A, B](ab: (A, B)) = Some(ab)
}

object :: {
  def unapply[K, V](m: Map[K, V]): Option[((K, V), Map[K, V])] = 
    m.headOption.map(_ -> m.tail)
}

scala> json match {
     |   case ("key1" -> "one") :: ("key2" -> value2) :: _ => value2
     | }
res0: java.lang.String = two

You would not be able to extract keys in the wrong order though

scala> json match {
     |   case ("key2" -> value2) :: _ => value2
     |   case _ => "match fail"
     | }
res2: java.lang.String = match fail

You could write Key1, Key2, Key3 as well. This may or may not scale well.

object && {
  def unapply[A](a: A) = Some((a, a))
}

object Key2 {
  def unapply[V](m: Map[String, V]) = m.get("key2")
}

object Key1 {
  def unapply[V](m: Map[String, V]) = m.get("key1")
}

scala> json match {
     |   case Key2(value2) && Key1(value1) => (value2, value1)
     | }
res5: (java.lang.String, java.lang.String) = (two,one)

Upvotes: 3

Related Questions