Scala/Play: parse JSON into Map instead of JsObject

前端 未结 5 1111
小蘑菇
小蘑菇 2020-12-24 14:04

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 co

相关标签:
5条回答
  • 2020-12-24 14:25

    I've chosen to use Jackson module for scala.

    import com.fasterxml.jackson.databind.ObjectMapper
    import com.fasterxml.jackson.module.scala.DefaultScalaModule
    import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
    
    val mapper = new ObjectMapper() with ScalaObjectMapper
    mapper.registerModule(DefaultScalaModule)
    val obj = mapper.readValue[Map[String, Object]](jsonString)
    
    0 讨论(0)
  • 2020-12-24 14:25

    For further reference and in the spirit of simplicity, you can always go for:

    Json.parse(jsonString).as[Map[String, JsValue]]
    

    However, this will throw an exception for JSON strings not corresponding to the format (but I assume that goes for the Jackson approach as well). The JsValue can now be processed further like:

    jsValueWhichBetterBeAList.as[List[JsValue]]
    

    I hope the difference between handling Objects and JsValues is not an issue for you (only because you were complaining about JsValues being proprietary). Obviously, this is a bit like dynamic programming in a typed language, which usually isn't the way to go (Travis' answer is usually the way to go), but sometimes that's nice to have I guess.

    0 讨论(0)
  • 2020-12-24 14:27

    Would recommend reading up on pattern matching and recursive ADTs in general to better understand of why Play Json treats JSON as a "first class citizen".

    That being said, many Java-first APIs (like Google Java libraries) expect JSON deserialized as Map[String, Object]. While you can very simply create your own function that recursively generates this object with pattern matching, the simplest solution would probably be to use the following existing pattern:

    import com.google.gson.Gson
    import java.util.{Map => JMap, LinkedHashMap}
    
    val gson = new Gson()
    
    def decode(encoded: String): JMap[String, Object] =   
       gson.fromJson(encoded, (new LinkedHashMap[String, Object]()).getClass)
    

    The LinkedHashMap is used if you would like to maintain key ordering at the time of deserialization (a HashMap can be used if ordering doesn't matter). Full example here.

    0 讨论(0)
  • 2020-12-24 14:32

    You can simply extract out the value of a Json and scala gives you the corresponding map. Example:

       var myJson = Json.obj(
              "customerId" -> "xyz",
              "addressId" -> "xyz",
              "firstName" -> "xyz",
              "lastName" -> "xyz",
              "address" -> "xyz"
          )
    

    Suppose you have the Json of above type. To convert it into map simply do:

    var mapFromJson = myJson.value
    

    This gives you a map of type : scala.collection.immutable.HashMap$HashTrieMap

    0 讨论(0)
  • 2020-12-24 14:35

    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.

    0 讨论(0)
提交回复
热议问题