Search code examples
scala

Safely reading a multilayered GenericRecord in Scala with Option


I have a question regarding the Option calls. I have a GenericRecord (i) parsed from a JSON object which from which I want to extract value (see code below):

Option(i.get("Hours_field").asInstanceOf[GenericRecord].get("Hour").asInstanceOf[Double]).getOrElse(-1.0)

This work fine for most cases but in a few events "Hours_field" is "null" which results in an error.

This of course could be fixed with an if else statement:

if (i.get("Hours_field")==null)
{
-1
}
else
{
Option(i.get("Hours_field").asInstanceOf[GenericRecord].get("Hour").asInstanceOf[Double]).getOrElse(-1.0)
}

But some sources said that this was an antipattern in Scala. Does anyone know a better way? The "when" option from this source sounds promising, but I cannot get it to work. I also tried the map function as described here but most likely I am using it wrong. Could anyone explain to me how I can be done? I am still quite new to Scala so excuse the maybe very obvious question.


Solution

  • I don't know what kind of API GenericRecord has, but probably something like this would be better:

    import scala.util.Try
    
    final class SafeRecord(val record: Option[GenericRecord]) {
    
      def get(name: String): SafeRecord = record.flatMap { r =>
        SafeRecord(r.get("name")))
      }
    
      def getDouble(name: String): Option[Double] = record.flatMap { f =>
        Try(r.get(name).asInstanceOf[Double]).toOption
      }
    }
    object SafeRecord {
    
      def apply(record: GenericRecord): SafeRecord =
        new SafeRecord(Option(record))
    }
    

    Used as:

    SafeRecord(i).get("Hours_field").getDouble("Hour").getOrElse(-1.0)
    

    Normally, one doesn't have to play around with such wrappers, as Scala libraries don't use null, nor return an arbitrary Object/Any to downcast, but apparently this it one of "these" Java API so one has to wrap it up to have some safe API.

    You could also just use Options with flatMaps:

    // I don't know if in your case i can be null
    Option(i.get("Hours_field"))
      .flatMap(k => Option(k.get("Hours")))
      .flatMap(d => Try(d.asInstanceOf[Double].toOption))
      .getOrElse(-1.0)
    

    or for comprehension

    val d = for {
      j <- Option(i.get("Hours_field"))
      k <- Option(j.get("Hours"))
      l <- Try(l.asInstanceOf[Double]).toOption
    } yield l
    d.getOrElse(-1.0)
    

    But for such particular APIs quasi-optics or paths could be more readable.

    Of course, this issue is already solved if you use Scala libraries for JSON, because they can have decoders/encoders generated for you or allow using JSON optics/paths.