Search code examples
jsonscalaplayframeworklibslagom

Generic Play json Formatter


I have a scala application and have a case class like -

case class SR(
  systemId: Option[String] = None,
  x: Map[Timestamp, CaseClass1] = Map.empty,
  y: Map[Timestamp, CaseClass2] = Map.empty,
  y: Map[Timestamp, CaseClass3] = Map.empty
)

Now I have to provide an implicit read and write JSON format for properties x,y,z for SR case class like -

  implicit val mapCMPFormat = new Format[Map[Timestamp, CaseClass1]] {
    def writes(obj: Map[Timestamp, CaseClass1]): JsValue =
      JsArray(obj.values.toSeq.map(Json.toJson(_)))
    def reads(jv: JsValue): JsResult[Map[Timestamp, CaseClass1]] = jv.validate[scala.collection.Seq[CaseClass1]] match {
      case JsSuccess(objs, path) => JsSuccess(objs.map(obj => obj.dataDate.get -> obj).toMap, path)
      case err: JsError => err
    }
  }

And so on similarly for Y and Z, and in future, I will be adding many more properties like x,y,z in SR case class and then need to provide the formators.

So Can I get some Generic Formater that will take care for all types?


Solution

  • To my knowledge, a simple way to achieve this does not exists, however, to create a "default" reader for each object should not be hard to do, something like:

    case class VehicleColorForAdd(
      name: String,
      rgb: String
    )
    
    object VehicleColorForAdd {
      implicit val jsonFormat: Format[VehicleColorForAdd] = Json.formats[VehicleColorForAdd]
    }
    

    This way you have access to the implicit by simply using the object, so you could have other objects that contains this object with no problem:

    case class BiggerModel(
      vehicleColorForAdd: VehicleColorForAdd
    )
    
    object BiggerModel{
      implicit val jsonFormat: Format[BiggerModel] = Json.format[BiggerModel]
    }
    

    Sadly, you need to do this for each class type, but you can "extend" play converters with your own, for example, this are some of my default readers:

    package common.json
    
    import core.order.Order
    import org.joda.time.{ DateTime, LocalDateTime }
    import org.joda.time.format.DateTimeFormat
    import core.promotion.{ DailySchedule, Period }
    import play.api.libs.functional.syntax._
    import play.api.libs.json.Reads._
    import play.api.libs.json._
    import play.api.libs.json.{ JsError, JsPath, JsSuccess, Reads }
    
    import scala.language.implicitConversions
    
    /**
     * General JSon readers and transformations.
     */
    object JsonReaders {
    
      val dateTimeFormat = "yyyy-MM-dd HH:mm:ss"
    
      class JsPathHelper(val path: JsPath) {
        def readTrimmedString(implicit r: Reads[String]): Reads[String] = Reads.at[String](path)(r).map(_.trim)
    
        def readUpperString(implicit r: Reads[String]): Reads[String] = Reads.at[String](path)(r).map(_.toUpperCase)
    
        def readNullableTrimmedString(implicit r: Reads[String]): Reads[Option[String]] = Reads.nullable[String](path)(r).map(_.map(_.trim))
      }
    
      implicit val localDateTimeReader: Reads[LocalDateTime] = Reads[LocalDateTime]((js: JsValue) =>
        js.validate[String].map[LocalDateTime](dtString =>
          LocalDateTime.parse(dtString, DateTimeFormat.forPattern(dateTimeFormat))))
    
      val localDateTimeWriter: Writes[LocalDateTime] = new Writes[LocalDateTime] {
        def writes(d: LocalDateTime): JsValue = JsString(d.toString(dateTimeFormat))
      }
    
      implicit val localDateTimeFormat: Format[LocalDateTime] = Format(localDateTimeReader, localDateTimeWriter)
    
      implicit val dateTimeReader: Reads[DateTime] = Reads[DateTime]((js: JsValue) =>
        js.validate[String].map[DateTime](dtString =>
          DateTime.parse(dtString, DateTimeFormat.forPattern(dateTimeFormat))))
    
      implicit def toJsPathHelper(path: JsPath): JsPathHelper = new JsPathHelper(path)
    
      val defaultStringMax: Reads[String] = maxLength[String](255)
    
      val defaultStringMinMax: Reads[String] = minLength[String](1) andKeep defaultStringMax
    
      val rgbRegex: Reads[String] = pattern("""^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})$""".r, "error.invalidRGBPattern")
    
      val plateRegex: Reads[String] = pattern("""^[\d\a-zA-Z]*$""".r, "error.invalidPlatePattern")
    
      val minOnlyWordsRegex: Reads[String] = minLength[String](2) keepAnd onlyWordsRegex
    
      val positiveInt: Reads[Int] = min[Int](1)
    
      val zeroPositiveInt: Reads[Int] = min[Int](0)
    
      val zeroPositiveBigDecimal: Reads[BigDecimal] = min[BigDecimal](0)
    
      val positiveBigDecimal: Reads[BigDecimal] = min[BigDecimal](1)
    
      def validLocalDatePeriod()(implicit reads: Reads[Period]) =
        Reads[Period](js => reads.reads(js).flatMap { o =>
          if (o.startPeriod isAfter o.endPeriod)
            JsError("error.startPeriodAfterEndPeriod")
          else
            JsSuccess(o)
        })
    
      def validLocalTimePeriod()(implicit reads: Reads[DailySchedule]) =
        Reads[DailySchedule](js => reads.reads(js).flatMap { o =>
          if (o.dailyStart isAfter o.dailyEnd)
            JsError("error.dailyStartAfterDailyEnd")
          else
            JsSuccess(o)
        })
    }
    

    Then, you only need to import this object to have access to all this implicit converters:

    package common.forms
    
    import common.json.JsonReaders._
    import play.api.libs.json._
    
    /**
     * Form to add a model with only one string field.
     */
    object SimpleCatalogAdd {
    
      case class Data(
        name: String
      )
    
      implicit val dataReads: Reads[Data] = (__ \ "name").readTrimmedString(defaultStringMinMax).map(Data.apply)
    }