Search code examples
scalaujsonupickle

Idiomatic handling of JSON null in scala upickle / ujson


I am new to Scala and would like to learn the idiomatic way to solve common problems, as in pythonic for Python. My question regards reading JSON data with upickle, where the JSON value contains a string when present, and null when not present. I want to use a custom value to replace null. A simple example:

import upickle.default._

val jsonString = """[{"always": "foo", "sometimes": "bar"}, {"always": "baz", "sometimes": null}]"""
val jsonData = ujson.read(jsonString)

for (m <- jsonData.arr) {
  println(m("always").str.length)  // this will work
  println(m("sometimes").str.length)  // this will fail, Exception in thread "main" ujson.Value$InvalidData: Expected ujson.Str (data: null)
}

The issue is with the field "sometimes": when null, we cannot apply .str (or any other function mapping to a static type other than null). I am looking for something like m("sometimes").str("DEFAULT").length, where "DEFAULT" is the replacement for null.

Idea 1 Using pattern matching, the following works:

val sometimes = m("sometimes") match {
  case s: ujson.Str => s.str
  case _ => "DEFAULT"
}
println(sometimes.length)

Given Scala's concise syntax, this looks a bit complicated and will be repetitive when done for a number of values.

Idea 2 Answers to a related question mention creating a case class with default values. For my problem, the creation of a case class seems inflexible to me when different replacement values are needed depending depending on context.

Idea 3 Anwers to another question (not specific to upickle) discuss using Try().getOrElse(), i.e.:

import scala.util.Try
// ...
println(Try(m("sometimes").str).getOrElse("DEFAULT").length)

However, the discussion mentions that throwing an exception for a regular program path is expensive.

What are idiomatic, yet concise ways to solve this?


Solution

  • Idiomatic or scala way to do this by using scala's Option.

    Fortunately, upickle Values offers them. Refer strOpt method in this source code.

    Your problem in code is str methods in m("always").str and m("sometimes").str With this code, you are prematurely assuming that all the values are strings. That's where the strOpt method comes. It either outputs a string if its value is a string or a None type if it not. And we can use getOrElse method coupled with it to decide what to throw if the value is None.

    Following would be the optimum way to handle this.

    val jsonString = """[{"always": "foo", "sometimes": "bar"}, {"always": "baz", "sometimes": null}]"""
    
    for (m <- jsonData.arr) {
        println(m("always").strOpt.getOrElse("").length)  
        println(m("sometimes").strOpt.getOrElse("").length) 
      } 
    

    Output:

    3
    3
    3
    0
    

    Here if we get any value other than a string (null, float, int), the code will output it as an empty string. And its length will be calculated as 0.

    Basically, this is similar to your "Idea1" approach but this is the scala way. Instead of "DEFAULT", I am throwing an empty string because you wouldn't want to have null values' length to be 7 (Length of string "DEFAULT").