I have a Scala (Play!) application that must get and parse some data in JSON from an external service. I want to be able to gently handle failure in the response format, but it is becoming messy. What I am doing right now is, for instance,
def columnsFor(json: JsValue): Option[Map[String, String]] =
try {
val cols = (json \ "responseBody" \ "columns") match {
case JsArray(xs) => xs map { col =>
((col \ "leafid").as[String], (col \ "name").as[String])
}
case _ => throw new Exception("Response unparsable")
}
Some(cols.toMap)
}
catch {
case e: Exception => None
}
That is, I go parsing optimistically and throw an exception whenever I find something that does not match my expected format (the methods as[String]
also raise exceptions when they do not find strings). Then I recover from the exception and give back a None
.
This approach is working and allows me to go from a potentially broken textual response to some type-safe data.
The problem is that having all these try..catch
blocks looks ugly and messy. In theory it would be nicer to use optional types during all parsing steps. But then all my data structures become messy, as at each level you must have Option
s. When the data is deeply nested, I do not know how to handle failure at all levels. Moreover, I do not want to return something like an Option[Map[Option[String], Option[String]]
, but just an Option[Map[String, String]]
.
Is there some better/more idiomatic way to handle failures during parsing?
EDIT
I try to make the question more clear. Say I am expecting a response like
{
"foo": [1, 2, 3],
"bar": [2, 5]
}
I want to be able to parse it; taking failure into account I want to get something of type Option[Map[String, List[Int]]]
. Now the problem is that parsing errors can appear at any level. Maybe I get a map with string keys, but its values are not lists. Or maybe the values are lists, but the content of the lists are strings instead of ints. And so on.
Whenever I want to parse a value, say as a string, I can choose between x.as[String]
and x.asOpt[String]
. The former will throw an exception when dealing with wrong input, while the latter returns a Option[String]
. Similarly, when dealing with an array, I can do pattern matching like
x match {
case JsArray(xs) => // continue parsing
case _ => // deal with failure
}
I can deal with failure returning None
or with an exception.
If I choose to use exceptions all along, then I can just wrap everything inside a try..catch
and be sure to have handled all failures. Otherwise, my problem is that at any level, I have to put an option. So I have to
- get something with an awful type like
Option[Map[Option[String], Option[List[Option[Int]]]]]
- find some way to flatten this all the way down to turn into the more manageable
Option[Map[String, List[Int]]]
Of course, thing become even worse as soon as the JSON gets more nested.
Future()
=) \$\endgroup\$