Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala/Play: parse JSON into Map instead of JsObject

Tags:

On Play Framework's homepage they claim that "JSON is a first class citizen". I have yet to see the proof of that.

In my project I'm dealing with some pretty complex JSON structures. This is just a very simple example:

{     "key1": {         "subkey1": {             "k1": "value1"             "k2": [                 "val1",                 "val2"                 "val3"             ]         }     }     "key2": [         {             "j1": "v1",             "j2": "v2"         },         {             "j1": "x1",             "j2": "x2"         }     ] } 

Now I understand that Play is using Jackson for parsing JSON. I use Jackson in my Java projects and I would do something simple like this:

ObjectMapper mapper = new ObjectMapper(); Map<String, Object> obj = mapper.readValue(jsonString, Map.class); 

This would nicely parse my JSON into Map object which is what I want - Map of string and object pairs and would allow me easily to cast array to ArrayList.

The same example in Scala/Play would look like this:

val obj: JsValue = Json.parse(jsonString) 

This instead gives me a proprietary JsObject type which is not really what I'm after.

My question is: can I parse JSON string in Scala/Play to Map instead of JsObject just as easily as I would do it in Java?

Side question: is there a reason why JsObject is used instead of Map in Scala/Play?

My stack: Play Framework 2.2.1 / Scala 2.10.3 / Java 8 64bit / Ubuntu 13.10 64bit

UPDATE: I can see that Travis' answer is upvoted, so I guess it makes sense to everybody, but I still fail to see how that can be applied to solve my problem. Say we have this example (jsonString):

[     {         "key1": "v1",         "key2": "v2"     },     {         "key1": "x1",         "key2": "x2"     } ] 

Well, according to all the directions, I now should put in all that boilerplate that I otherwise don't understand the purpose of:

case class MyJson(key1: String, key2: String) implicit val MyJsonReads = Json.reads[MyJson] val result = Json.parse(jsonString).as[List[MyJson]] 

Looks good to go, huh? But wait a minute, there comes another element into the array which totally ruins this approach:

[     {         "key1": "v1",         "key2": "v2"     },     {         "key1": "x1",         "key2": "x2"     },     {         "key1": "y1",         "key2": {             "subkey1": "subval1",             "subkey2": "subval2"         }     } ] 

The third element no longer matches my defined case class - I'm at square one again. I am able to use such and much more complicated JSON structures in Java everyday, does Scala suggest that I should simplify my JSONs in order to fit it's "type safe" policy? Correct me if I'm wrong, but I though that language should serve the data, not the other way around?

UPDATE2: Solution is to use Jackson module for scala (example in my answer).

like image 321
Caballero Avatar asked Nov 17 '13 10:11

Caballero


People also ask

How do I read a JSON file into a map?

In order to convert JSON data into Java Map, we take help of JACKSON library. We add the following dependency in the POM. xml file to work with JACKSON library. Let's implement the logic of converting JSON data into a map using ObjectMapper, File and TypeReference classes.

What is a JsValue?

JsValue can be a string, numeric, object or array. Therefore you can't assume that it has key value pairs. If you have a val x: JsValue that you know is a JsObject you can use x.as[JsObject] to cast it or if you're not sure it's a JsObject you can use x.

What is the best JSON library for Scala?

Why? Argonaut is a great library. It's by far the best JSON library for Scala, and the best JSON library on the JVM. If you're doing anything with JSON in Scala, you should be using Argonaut.


1 Answers

Scala in general discourages the use of downcasting, and Play Json is idiomatic in this respect. Downcasting is a problem because it makes it impossible for the compiler to help you track the possibility of invalid input or other errors. Once you've got a value of type Map[String, Any], you're on your own—the compiler is unable to help you keep track of what those Any values might be.

You have a couple of alternatives. The first is to use the path operators to navigate to a particular point in the tree where you know the type:

scala> val json = Json.parse(jsonString) json: play.api.libs.json.JsValue = {"key1": ...  scala> val k1Value = (json \ "key1" \ "subkey1" \ "k1").validate[String] k1Value: play.api.libs.json.JsResult[String] = JsSuccess(value1,) 

This is similar to something like the following:

val json: Map[String, Any] = ???  val k1Value = json("key1")   .asInstanceOf[Map[String, Any]]("subkey1")   .asInstanceOf[Map[String, String]]("k1") 

But the former approach has the advantage of failing in ways that are easier to reason about. Instead of a potentially difficult-to-interpret ClassCastException exception, we'd just get a nice JsError value.

Note that we can validate at a point higher in the tree if we know what kind of structure we expect:

scala> println((json \ "key2").validate[List[Map[String, String]]]) JsSuccess(List(Map(j1 -> v1, j2 -> v2), Map(j1 -> x1, j2 -> x2)),) 

Both of these Play examples are built on the concept of type classes—and in particular on instances of the Read type class provided by Play. You can also provide your own type class instances for types that you've defined yourself. This would allow you to do something like the following:

val myObj = json.validate[MyObj].getOrElse(someDefaultValue)  val something = myObj.key1.subkey1.k2(2) 

Or whatever. The Play documentation (linked above) provides a good introduction to how to go about this, and you can always ask follow-up questions here if you run into problems.


To address the update in your question, it's possible to change your model to accommodate the different possibilities for key2, and then define your own Reads instance:

case class MyJson(key1: String, key2: Either[String, Map[String, String]])  implicit val MyJsonReads: Reads[MyJson] = {   val key2Reads: Reads[Either[String, Map[String, String]]] =     (__ \ "key2").read[String].map(Left(_)) or     (__ \ "key2").read[Map[String, String]].map(Right(_))    ((__ \ "key1").read[String] and key2Reads)(MyJson(_, _)) } 

Which works like this:

scala> Json.parse(jsonString).as[List[MyJson]].foreach(println) MyJson(v1,Left(v2)) MyJson(x1,Left(x2)) MyJson(y1,Right(Map(subkey1 -> subval1, subkey2 -> subval2))) 

Yes, this is a little more verbose, but it's up-front verbosity that you pay for once (and that provides you with some nice guarantees), instead of a bunch of casts that can result in confusing runtime errors.

It's not for everyone, and it may not be to your taste—that's perfectly fine. You can use the path operators to handle cases like this, or even plain old Jackson. I'd encourage you to give the type class approach a chance, though—there's a steep-ish learning curve, but lots of people (including myself) very strongly prefer it.

like image 128
Travis Brown Avatar answered Oct 10 '22 11:10

Travis Brown