1
votes

I am trying to parse json with null values for some fields using Play library. There is a case class which represents the data:

case class Id(value: Int) extends AnyVal

case class Name(value: String) extends AnyVal

case class Number(value: Int) extends AnyVal

case class Data(id: Option[Id], name: Option[Name], number: Option[Number])

Here is how parsing currently works:

def parse(jsValue: JsValue): Try[Seq[Data]] = Try {
    jsValue.as[JsArray].value
      .flatMap { record =>
        val id = Id((record \ "id").as[Int])
        val name = Name((record \ "name").as[String])
        val number = Number((record \ "number").as[Int])

        Some(Data(Some(id), Some(name), Some(number)))
      }
  }

Parsing with specific data types doesn't handle null cases, so this implementation returns:

Failure(play.api.libs.json.JsResultException: JsResultException(errors:List((,List(JsonValidationError(List(error.expected.jsstring),WrappedArray()))))))

For the input data like this:

{
    "id": 1248,
    "default": false,
    "name": null,
    "number": 2
  }

I would like to have something like this: Seq(Data(Some(Id(1248)), None, Some(Number(2))))

I am going to write the data into the database so I do not mind writing some null values for these fields.

How can I handle null values for fields in parsed json?

2
Does your JSON contain a trailing comma following the last key/value pair?Andriy Plokhotnyuk
No, it does not. It is accidentalCassie

2 Answers

2
votes

You can simply let the play-json library generate the Reads for your case classes instead of writing them manually:

import play.api.libs.json._

object Data {
  implicit val reads: Reads[Data] = {
    // move these into the corresponding companion objects if used elsewhere...
    implicit val idReads = Json.reads[Id]
    implicit val numberReads = Json.reads[Number]
    implicit val nameReads = Json.reads[Name]

    Json.reads[Data]
  }
}

def parse(jsValue: JsValue): Try[Seq[Data]] = Json.fromJson[Seq[Data]](jsValue).toTry

That way, your code will work even in case you change the arguments of your case classes.

If you still want to code it manually, you can use the readNullable parser:

val name: Option[Name] = Name(record.as((__ \ "name").readNullable[String]))

Note, however, that using Try is somewhat frowned upon in FP and directly using JsResult would be more idiomatic.

0
votes

If you are not locked to use play-json then let me show how it can be done easily with jsoniter-scala:

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._

implicit val codec: JsonValueCodec[Seq[Data]] = JsonCodecMaker.make(CodecMakerConfig)

val json: Array[Byte] = """[
  {
    "id": 1248,
    "default": false,
    "name": null,
    "number": 2
  }
]""".getBytes("UTF-8")
val data: Seq[Data] = readFromArray(json)
println(data)

That will produce the following output:

List(Data(Some(Id(1248)),None,Some(Number(2))))

Here you can see an example how to integrate it with the Play framework.