JSON to XML in Scala and dealing with Option() res

2019-02-26 22:04发布

Consider the following from the Scala interpreter:

scala> JSON.parseFull("""{"name":"jack","greeting":"hello world"}""")
res6: Option[Any] = Some(Map(name -> jack, greeting -> hello world))

Why is the Map returned in Some() thing? And how do I work with it?

I want to put the values in an xml template:

<test>
  <name>name goes here</name>
  <greeting>greeting goes here</greeting>
</test>

What is the Scala way of getting my map out of Some(thing) and getting those values in the xml?

4条回答
Root(大扎)
2楼-- · 2019-02-26 22:40

You have two separate problems.

  1. It's typed as Any.
  2. Your data is inside an Option and a Map.

Let's suppose we have the data:

val x: Option[Any] = Some(Map("name" -> "jack", "greeting" -> "hi"))

and suppose that we want to return the appropriate XML if there is something to return, but not otherwise. Then we can use collect to gather those parts that we know how to deal with:

val y = x collect {
  case m: Map[_,_] => m collect {
    case (key: String, value: String) => key -> value
  }
}

(note how we've taken each entry in the map apart to make sure it maps a string to a string--we wouldn't know how to proceed otherwise. We get:

y: Option[scala.collection.immutable.Map[String,String]] =
  Some(Map(name -> jack, greeting -> hi))

Okay, that's better! Now if you know which fields you want in your XML, you can ask for them:

val z = for (m <- y; name <- m.get("name"); greet <- m.get("greeting")) yield {
  <test><name>{name}</name><greeting>{greet}</greeting></test>
}

which in this (successful) case produces

z: Option[scala.xml.Elem] =
  Some(<test><name>jack</name><greeting>hi</greeting></test>)

and in an unsuccessful case would produce None.

If you instead want to wrap whatever you happen to find in your map in the form <key>value</key>, it's a bit more work because Scala doesn't have a good abstraction for tags:

val z = for (m <- y) yield <test>{ m.map { case (tag, text) => xml.Elem(null, tag, xml.Null, xml.TopScope, xml.Text(text)) }}</test>

which again produces

z: Option[scala.xml.Elem] =
  Some(<test><name>jack</name><greeting>hi</greeting></test>)

(You can use get to get the contents of an Option, but it will throw an exception if the Option is empty (i.e. None).)

查看更多
Rolldiameter
3楼-- · 2019-02-26 22:49

parseFull returns an Option because the string may not be valid JSON (in which case it will return None instead of Some).

The usual way to get the value out of a Some is to pattern match against it like this:

result match {
    case Some(map) =>
        doSomethingWith(map)
    case None =>
        handleTheError()
}

If you're certain the input will always be valid and so you don't need to handle the case of invalid input, you can use the get method on the Option, which will throw an exception when called on None.

查看更多
Anthone
4楼-- · 2019-02-26 22:55

You should probably use something like this:

res6 collect { case x: Map[String, String] => renderXml(x) }

Where:

def renderXml(m: Map[String, String]) = 
  <test><name>{m.get("name") getOrElse ""}</name></test>

The collect method on Option[A] takes a PartialFunction[A, B] and is a combination of filter (by a predicate) and map (by a function). That is:

opt collect pf
opt filter (a => pf isDefinedAt a) map (a => pf(a))

Are both equivalent. When you have an optional value, you should use map, flatMap, filter, collect etc to transform the option in your program, avoiding extracting the option's contents either via a pattern-match or via the get method. You should never, ever use Option.get - it is the canonical sign that you are doing it wrong. Pattern-matching should be avoided because it represents a fork in your program and hence adds to cyclomatic complexity - the only time you might wish to do this might be for performance


Actually you have the issue that the result of the parseJSON method is an Option[Any] (the reason is that it is an Option, presumably, is that the parsing may not succeed and Option is a more graceful way of handling null than, well, null).

But the issue with my code above is that the case x: Map[String, String] cannot be checked at runtime due to type erasure (i.e. scala can check that the option contains a Map but not that the Map's type parameters are both String. The code will get you an unchecked warning.

查看更多
聊天终结者
5楼-- · 2019-02-26 22:56

An Option is returned because parseFull has different possible return values depending on the input, or it may fail to parse the input at all (giving None). So, aside from an optional Map which associates keys with values, an optional List can be returned as well if the JSON string denoted an array.

Example:

scala> import scala.util.parsing.json.JSON._
import scala.util.parsing.json.JSON._

scala> parseFull("""{"name":"jack"}""")
res4: Option[Any] = Some(Map(name -> jack))

scala> parseFull("""[ 100, 200, 300 ]""")
res6: Option[Any] = Some(List(100.0, 200.0, 300.0))

You might need pattern matching in order to achieve what you want, like so:

scala> parseFull("""{"name":"jack","greeting":"hello world"}""") match {
     |   case Some(m) => Console println ("Got a map: " + m)
     |   case _ =>
     | }
Got a map: Map(name -> jack, greeting -> hello world)

Now, if you want to generate XML output, you can use the above to iterate over the key/value pairs:

import scala.xml.XML

parseFull("""{"name":"jack","greeting":"hello world"}""") match {
  case Some(m: Map[_,_]) =>
    <test>
      {
        m map { case (k,v) =>
          XML.loadString("<%s>%s</%s>".format(k,v,k))
        }
      }
    </test>

  case _ =>
}
查看更多
登录 后发表回答