Search code examples
scalaplayframeworkplay-json

Handling nulls with json Play


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?


Solution

  • 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.