Parse a property that can be string or int

scala

(Davide Icardi) #1

I’m new to Play Framework and Scala. I have a json like this:

{
	"data" : [
		{
			"key" : "key1",
			"value" : "value 1"
		},
		{
			"key" : "key2",
			"value" : 2
		}
	]
}

As you can see the value property can be a String or an Int.
What is the best way to parse it?

My first idea was to create a case class with an Any type for the value property:

case class MyData(key: String, value: Any)

But I don’t know how to parse it. I have tried with:

implicit val dataReads: Reads[MyData] = (
  (JsPath \ "key").read[String] and
    (JsPath \ "value").read[Any]
  )(MyData.apply _)

But I get a compile error:

No Json deserializer found for type Any. Try to implement an implicit Reads or Format for this type.

Any idea?
Thanks


(Jules Ivanic) #2

Maybe you can implement it with an Either ? Either[Int, String]


(Davide Icardi) #3

Thanks Jules,

Using Either is for sure better than using Any on my case class. But I’m always stuck on parser side.
If I write:

(JsPath \ "value").read[Either[String, Int]]

I always get a similar error:

No Json deserializer found for type Either[String, Int]. Try to implement an implicit Reads or Format for this type.

I suppose that Either is not natively supported, that is fine for me. But I don’t understand how to insert some custom logic when using Reads. Or should I parse the object in another way?

thanks


(Jules Ivanic) #4

You need to implement at least this:

implicit def eitherReads[L: Reads, R: Reads]: Reads[Either[L, R]] = ???

You’re not the first one needing this. For example, there’s a solution proposed here (I didn’t read it, I’m on a phone. It’s not practical): https://stackoverflow.com/questions/39382669/play-scala-json-format-for-either


(Schmitt Christian) #5

Well you are probably looking for something like that:

  sealed trait MyValue
  case class MyString(s: String) extends MyValue
  case class MyInt(i: Int) extends MyValue
  implicit val myValueReads: Reads[MyValue] = new Reads[MyValue] {
    override def reads(json: JsValue): JsResult[MyValue] = {
      json match {
        case JsString(s) => JsSuccess(MyString(s))
        case other => Reads.IntReads.reads(other).map(MyInt)
      }
    }
  }

(Davide Icardi) #6

Thanks very much @schmitch! It works! Here the full code for future reference:

sealed trait DataValue
case class DataValueString(s: String) extends DataValue
case class DataValueInt(i: Int) extends DataValue

case class MyData(key: String, value: DataValue)

private implicit val dataValueReads: Reads[DataValue] = new Reads[DataValue] {
  override def reads(json: JsValue): JsResult[DataValue] = {
    json match {
      case JsString(s) => JsSuccess(DataValueString(s))
      case JsNumber(s) => JsSuccess(DataValueInt(s.toInt))
      case _ => throw new Exception("Invalid json value") // TODO Add better error handling
    }
  }
}

implicit val dataReads: Reads[MyData] = (
  (JsPath \ "key").read[String] and
    (JsPath \ "value").read[DataValue]
  )(MyData.apply _)