No Json formatter for Option[String]?
Asked Answered
S

2

11

I am trying to marshall and un-marshall an Option[String] field to and from JSON. For my use-case, a None value should be marshaled as "null". Here is the code I have:

import org.scalatest.{FlatSpec, Matchers}

import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._


case class Person(
  id: Int,
  firstName: Option[String],
  lastName: Option[String]
)

object Person {
  implicit lazy val personFormat = (
    (__ \ "id").format[Int] and
    (__ \ "first_name").format[Option[String]] and
    (__ \ "last_name").format[Option[String]]
  )(Person.apply, unlift(Person.unapply))
}

class PersonSpec extends FlatSpec with Matchers {
  "When Person instance is marshaled None fields " should
    "be serialized as \"null\" values" in {
    val person = Person(1, None, None)
    import Person._
    val json = Json.toJson(person)
    println(json)
    (json \ "id").as[Int] should be (1)
    (json \ "first_name").get should be (JsNull)
    (json \ "last_name").get should be (JsNull)
  }
}

This results in the following compiler error:

PersonSpec.scala:19: No Json formatter found for type Option[String]. Try to implement an implicit Format for this type.
[error]     (__ \ "first_name").format[Option[String]] and
[error]                               ^

These are some of the things I have tried:

Replacing (__ \ "first_name").format[Option[String]] with (__ \ "first_name").formatNullable[String] makes the compiler happy, but the test fails (""java.util.NoSuchElementException: None.get"") with the following output (from println(json))

{"id":1}

This confirms with formatNullable's behavior (don't render None valued fields).

Next, I replaced the format with a writes. Like so:

object Person {
  implicit lazy val personWrite = (
    (__ \ "id").write[Int] and
    (__ \ "first_name").write[Option[String]] and
    (__ \ "last_name").write[Option[String]]
  )(unlift(Person.unapply))
}

Now, the compiler is happy and the test passes.

But I now need to implement a separate Reads. If I could, I would rather not as it violates DRY principle.

What am I doing wrong and when write[Option[...]] works perfectly why not format[Option[...]]?

Sunrise answered 26/3, 2017 at 16:29 Comment(0)
K
16

Adding this code so that it is implicit-visible from your PersonFormat will make it work.

implicit def optionFormat[T: Format]: Format[Option[T]] = new Format[Option[T]]{
    override def reads(json: JsValue): JsResult[Option[T]] = json.validateOpt[T]

    override def writes(o: Option[T]): JsValue = o match {
      case Some(t) ⇒ implicitly[Writes[T]].writes(t)
      case None ⇒ JsNull
    }
  }

I think that in play it is assumed that option-valued fields should be treated optional at all, hence the behaviour you observed with formatNullable.

Koto answered 26/3, 2017 at 17:6 Comment(0)
H
7

You can use:

(__ \ "first_name").formatNullable[String]
Hagbut answered 14/11, 2017 at 6:19 Comment(1)
Exactly what I needed to read the Optional[String], thank you!Cardoso

© 2022 - 2024 — McMap. All rights reserved.