How to format generic type as json using play json api?

78 views Asked by At

I have a function which take a json string, parse it, fetch some values based on the condition and convert JsValue of different types:

trait Module
case class Module1(dp: String, qu: String, us: String) extends Module
case class Module2(dp: String, qu: String) extends Module
case class Module3(dp: String) extends Module
case class EmptyModule() extends Module
def getModule: (String, String) => String = (str1: String, str2: String) => {
  modJsonString = if(str1.nonEmpty && str2.nonEmpty){
    val modObject = Json.parse(str).as[JsObject]
    val dp = (modObject \ "dp").as[String]
    val qu = (modObject \ "qu").as[String]
    val us = (modObject \ "us").as[String]
    Module1(dp,qu,us)
  } else if(str1.nonEmpty){
    val dp = (modObject \ "dp").as[String]
    val qu = (modObject \ "qu").as[String]
    Module2(dp,qu)
  } else if(str2.nonEmpty){
    val dp = (modObject \ "dp").as[String]
    Module2(dp)
  }else EmptyModule()
  implicit val mod = Json.format[Module]
  val jsonValue = Json.toJson(modJsonString)
  jsonValue.toString()
}

While executing this function, I am getting error:

No apply function found for Module
implicit val mod = Json.format[Module]

Can anyone pls help to format a generic class or trait.

2

There are 2 answers

0
Gaël J On BEST ANSWER

There are several ways to implement a Writes for a trait (I'm not talking about Reads as your example only needs a serializer).

The "most usual" is to implement one for each possible subtype and implement one for the trait that reuses the ones of each subtype.

Something like:

implicit val writes1: Writes[Module1] = ...
implicit val writes2: Writes[Module2] = ...
...

implicit val writes: Writes[Module] = Writes {
  case m: Module1 => writes1.apply(m)
  case m: Module2 => writes2.apply(m)
  ...
}

Depending on whether you'll need the serializers for the subtypes on their own they can be kept as is or made private or even inlined in the serializer of the trait.

I believe there are other ways, including some libraries that may generate all of this for you but I won't cover this here as I don't know them much.


That being said, your whole code could probably be simplified a lot.

Your original code doesn't compile and there are some unknowns (where I left ???) but it could look like the following:


implicit val format1: OFormat[Module1] = Json.format[Module1]
implicit val format2: OFormat[Module2] = Json.format[Module2]
...

def getModule: (String, String) => String = (str1: String, str2: String) => {
  val modJsonString = if(str1.nonEmpty && str2.nonEmpty){
    Json.parse(???).as[Module1]
  } else if(str1.nonEmpty){
    Json.parse(str1).as[Module2]
  } else if(str2.nonEmpty){
    Json.parse(str2).as[Module3]
  }else EmptyModule()
  
  Json.toJson(modJsonString).toString()
}
0
liath On

Generally speaking underneath Json.format[???] is a macro that requires the ??? to have apply and unapply methods defined in companion object. Case classes have this requirement met automatically but for normal classes and traits you'll have to supply them yourself

So you could achieve it with something like this

import play.api.libs.json._

trait Module

object Module {

  def apply(dp: String, qu: String, us: Option[String]): Module = {
    us match {
      case Some(us) => Module1(dp, qu, us)
      case None => Module2(dp, qu)
    }
  }

  def unapply(module: Module): Option[(String, String, Option[String])] = {
    module match {
      case Module1(dp, qu, us) => Some(dp, qu, Some(us))
      case Module2(dp, qu) => Some(dp, qu, None)
    }
  }

  implicit val format = Json.format[Module]
}

case class Module1(dp: String, qu: String, us: String) extends Module

case class Module2(dp: String, qu: String) extends Module

object Main extends App {
  val module: Module = Module1("qwe", "qwe", "qwe")
  val out = Module.format.writes(module)
  println(out)
}

Or you could even write your own OFormat[Module] but there is much more boilerplate in here

trait Module

object Module {

  implicit val format = OFormat[Module](
    (jsV: JsValue) => {
      // your custom read function
      JsSuccess(Module1("qwe", "qwe", "qwe"))
    },
    (m: Module) => {
      // your custom write function
      JsObject(Seq("qwe" -> JsString(m.toString)))
    }
  )
}

Json.format is described closer in Play documentation: https://www.playframework.com/documentation/2.8.x/ScalaJsonAutomated#Requirements

Also note that even if you apply such fix this will not advance your struggles much further as there are few more issues with your code such as:

  • val modObject is in an if scope and as such it cannot be used outside of it
  • You missed val in line 7
  • getModule is currently a function that returns a function but you could always just have a function that takes two parameters and returns one (i.e. def getModule(str1: String, str2: String): String)

I don't know exactly what are your goals and logic of the code you supplied but whatever they are I hope you will have fun as I believe it to be the most important part of programming