Search code examples
jsonscalaserializationoption-typeplay-json

Why isn't a member of type Option[String] being serialized?


I have a dummy class Foo which has three members:

import play.api.libs.json.Json

case class Foo(id: String, fooType: FooType, nextId: Option[String])

object Foo {
  implicit val fooReads = Json.reads[Foo]
  implicit val fooFormat = Json.format[Foo]
}

where FooType is defined as

import play.api.libs.json.Json

case class FooType(a: String, b: String)

object FooType {
  implicit val fooTypeReads = Json.reads[FooType]
  implicit val fooTypeFormat = Json.format[FooType]
}

I find some interesting behavior when serializing a Foo object. If I serialize a Foo to JSON, parse the JSONified Foo, I find that all the members are parsed correctly:

val id = "id"
val fooType = FooType("a", "b")
val nextId = None

val foo = Foo(id, fooType, nextId)
val jsonFoo = Json.toJson(foo)
val parsedFoo = Json.parse(jsonFoo.toString).as[Foo]

assert(parsedFoo == foo)
assert(parsedFoo.id == id)
assert(parsedFoo.fooType == fooType)
assert(parsedFoo.nextId.isEmpty)

This is good, because that's what I expect.

However, in my next test, I find that the nextId field is not serializable at all:

val id = "id"
val fooType = FooType("a", "b")
val nextId = None

val foo = Foo(id, fooType, nextId)
val jsonFoo = Json.toJson(foo)

assert((jsonFoo \ "id").as[String] == id)
assert((jsonFoo \ "fooType").as[FooType] == fooType)
assert((jsonFoo \ "nextId").as[Option[String]].isEmpty)

This fails with the following error:

Error:(38, 35) No Json deserializer found for type Option[String]. Try to implement an implicit Reads or Format for this type.
    assert((jsonFoo \ "nextId").as[Option[String]].isEmpty)

Error:(38, 35) not enough arguments for method as: (implicit fjs: play.api.libs.json.Reads[Option[String]])Option[String].
Unspecified value parameter fjs.
    assert((jsonFoo \ "nextId").as[Option[String]].isEmpty)

Similarly, I find that when I print the JSON object dumped by Json.toJson(foo), the nextId field is missing from the JSON object:

println(Json.prettyPrint(jsonFoo))

{
  "id" : "id",
  "fooType" : {
    "a" : "a",
    "b" : "b"
  }
}

I can, however, parse the nextId field with toOption; i.e.,

assert((jsonFoo \ "nextId").toOption.isEmpty)

How can my object be correctly parsed from JSON if one of its members isn't deserializable natively?


Solution

  • The nextId field is serializable, otherwise, you wouldn't have been able to write a Foo to JSON at all.

    jsonFoo: play.api.libs.json.JsValue = {"id":"id","fooType":{"a":"a","b":"b"}}
    

    The problem you're having is that there are no Reads for Option[A]. Options are handled specially by Play JSON. When using JSON combinators, we use readNullable[A] and writeNullable[A] instead of read[Option[A]] and write[Option[A]]. Likewise, when using methods to pull individual fields from a JsValue, calling as will not work because it requires an implicit Reads[A] for the type you give it (in this case a Reads[Option[String]], which does not exist).

    Instead, you need to use asOpt, which will correctly handle the Option underneath:

    scala> (jsonFoo \ "nextId").asOpt[String]
    res1: Option[String] = None
    

    nextId doesn't appear in the printed JSON because the value you are serializing is None. This is what is expected to happen. Since the value is optional, it gets omitted from the JSON (it's just undefined in JavaScript). If it has a value, it will appear:

    scala> Json.toJson(Foo("id", FooType("a", "b"), Option("abcdef")))
    res3: play.api.libs.json.JsValue = {"id":"id","fooType":{"a":"a","b":"b"},"nextId":"abcdef"}
    

    If something wasn't serializable with Play JSON, it simply wouldn't compile.