Search code examples
scalaliftjson4s

What `JObject(rec) <- someJArray` means inside for-comprehension


I'm learning Json4s library.

I have a json fragment like this:

{
    "records":[
        {
            "name":"John Derp",
            "address":"Jem Street 21"
        },
        {
            "name":"Scala Jo",
            "address":"in my sweet dream"
        }
    ]
}

And, I have Scala code, which converts a json string into a List of Maps, like this:

import org.json4s._
import org.json4s.JsonAST._
import org.json4s.native.JsonParser

  val json = JsonParser.parse( """{"records":[{"name":"John Derp","address":"Jem Street 21"},{"name":"Scala Jo","address":"in my sweet dream"}]}""")

  val records: List[Map[String, Any]] = for {
    JObject(rec) <- json \ "records"
    JField("name", JString(name)) <- rec
    JField("address", JString(address)) <- rec
  } yield Map("name" -> name, "address" -> address)

  println(records)

The output of records to screen gives this:

List(Map(name -> John Derp, address -> Jem Street 21), Map(name -> Scala Jo, address -> in my sweet dream))

I want to understand what the lines inside the for loop mean. For example, what is the meaning of this line:

JObject(rec) <- json \ "records"

I understand that the json \ "records" produces a JArray object, but why is it fetched as JObject(rec) at left of <-? What is the meaning of the JObject(rec) syntax? Where does the rec variable come from? Does JObject(rec) mean instantiating a new JObject class from rec input?

BTW, I have a Java programming background, so it would also be helpful if you can show me the Java equivalent code for the loop above.


Solution

  • You have the following types hierarchy:

      sealed abstract class JValue {
        def \(nameToFind: String): JValue = ???
        def filter(p: (JValue) => Boolean): List[JValue] = ???
      }
    
      case class JObject(val obj: List[JField]) extends JValue
      case class JField(val name: String, val value: JValue) extends JValue
      case class JString(val s: String) extends JValue
      case class JArray(val arr: List[JValue]) extends JValue {
        override def filter(p: (JValue) => Boolean): List[JValue] = 
          arr.filter(p)
      }
    

    Your JSON parser returns following object:

      object JsonParser {
        def parse(s: String): JValue = {
          new JValue {
            override def \(nameToFind: String): JValue =
              JArray(List(
                JObject(List(
                  JField("name", JString("John Derp")),
                  JField("address", JString("Jem Street 21")))),
                JObject(List(
                  JField("name", JString("Scala Jo")),
                  JField("address", JString("in my sweet dream"))))))
          }
        }
      }
    
      val json = JsonParser.parse("Your JSON")
    

    Under the hood Scala compiler generates the following:

      val res = (json \ "records")
        .filter(_.isInstanceOf[JObject])
        .flatMap { x =>
          x match {
            case JObject(obj) => //
              obj //
                .withFilter(f => f match {
                  case JField("name", _) => true
                  case _                 => false
                }) //
                .flatMap(n => obj.withFilter(f => f match {
                  case JField("address", _) => true
                  case _                    => false
                }).map(a => Map(
                  "name" -> (n.value match { case JString(name) => name }),
                  "address" -> (a.value match { case JString(address) => address }))))
          }
        }
    

    First line JObject(rec) <- json \ "records" is possible because JArray.filter returns List[JValue] (i.e. List[JObject]). Here each value of List[JValue] maps to JObject(rec) with pattern matching.

    Rest calls are series of flatMap and map (this is how Scala for comprehensions work) with pattern matching.

    I used Scala 2.11.4.

    Of course, match expressions above are implemented using series of type checks and casts.

    UPDATE:

    When you use Json4s library there is an implicit conversion from JValue to org.json4s.MonadicJValue. See package object json4s:

    implicit def jvalue2monadic(jv: JValue) = new MonadicJValue(jv)
    

    This conversion is used here: JObject(rec) <- json \ "records". First, json is converted to MonadicJValue, then def \("records") is applied, then def filter is used on the result of def \ which is JValue, then it is again implicitly converted to MonadicJValue, then def filter of MonadicJValue is used. The result of MonadicJValue.filter is List[JValue]. After that steps described above are performed.