Search code examples
scalaplayframeworkplayframework-2.0shapelessplay-json

play-json on AST with type parameters


I am trying to create play-json reads and writes for an AST that basically looks like this

abstract sealed trait Rule[A] {
    def roomId: Option[Long] = None
    def valid(in: A): Boolean
}

abstract sealed trait ValueRule[A, B] extends Rule[A] {
    def value: B
}

abstract sealed trait NoValueRule[A] extends Rule[A]
case class OnlyDuringWorkHours(override val roomId: Option[Long] = None) extends NoValueRule[((ResStart, ResEnd), Center)] {
    override def valid(in: ((ResStart, ResEnd), Center)): Boolean = true
}

case class MaxLeadTime(override val roomId: Option[Long] = None, override val value: Int) extends ValueRule[ResStart, Int] {
    override def valid(in: ResStart): Boolean = true
}

case class MaxDuration(override val roomId: Option[Long] = None, override val value: String) extends ValueRule[(ResStart, ResEnd), String] {
    override def valid(in: (ResStart, ResEnd)): Boolean = true
}

case class Rules(centerId: Long, ruleList: Seq[Rule[_]])

my attempt at doing this looks like this

object Rule {
    implicit def ruleReads[R, V](implicit rReads: Reads[R], vReads: Reads[V] = null): Reads[Rule[R]] = {
        val theVRead = Option(vReads)
        val nvr = ???

        if (Option(theVRead).isDefined) {
            val vr = ???
            __.read[ValueRule[R, V]](vr).map(x => x.asInstanceOf[Rule[R]]).orElse(__.read[NoValueRule[R]](nvr).map(x => x.asInstanceOf[Rule[R]]))
        } else {
            __.read[NoValueRule[R]](nvr).map(x => x.asInstanceOf[Rule[R]])
        }
    }
    implicit def ruleWrites[R, V](implicit rWrites: Writes[R], vWrites: Writes[V] = null): Writes[Rule[R]] = Writes[Rule[R]]{
        case nv: NoValueRule[R] => Json.writes[NoValueRule[R]].writes(nv)
        case v: ValueRule[R, V] => Json.writes[ValueRule[R, V]].writes(v)
    }
}
object ValueRule {
    implicit def valueRuleReads[R, V](implicit rReads: Reads[R], vReads: Reads[V]): Reads[ValueRule[R, V]] = {
        val mlt = Json.reads[MaxLeadTime]
        val md = Json.reads[MaxDuration]
         __.read[MaxDuration](md).map(x => x.asInstanceOf[ValueRule[R, V]])
        .orElse(
            __.read[MaxLeadTime](mlt).map(x => x.asInstanceOf[ValueRule[R, V]])
        )
    }
    implicit def valueRuleWrites[R, V](implicit rWrites: Writes[R], vWrites: Writes[V]): Writes[ValueRule[R, V]] = Writes[ValueRule[R, V]]{
        case mlt: MaxLeadTime => Json.writes[MaxLeadTime].writes(mlt)
        case md: MaxDuration => Json.writes[MaxDuration].writes(md)
    }
}

object NoValueRule {
    implicit def noValueRuleReads[R](implicit rReads: Reads[R]): Reads[NoValueRule[R]] = {
        val odwh = Json.reads[OnlyDuringWorkHours]
        __.read[OnlyDuringWorkHours](odwh).map(x => x.asInstanceOf[NoValueRule[R]])
    }
    implicit def noValueRuleWrites[R](implicit rWrites: Writes[R]): Writes[NoValueRule[R]] = Writes[NoValueRule[R]]{
        case odwh: OnlyDuringWorkHours => Json.writes[OnlyDuringWorkHours].writes(odwh)
    }
}
object OnlyDuringWorkHours {
    implicit val format: Format[OnlyDuringWorkHours] = Json.format[OnlyDuringWorkHours]
}

object MaxLeadTime {
    implicit val format: Format[MaxLeadTime] = Json.format[MaxLeadTime]
}
object MaxDuration {
    implicit val format: Format[MaxDuration] = Json.format[MaxDuration]
}

object Rules {
    import play.api.libs.json.Reads._
    import play.api.libs.functional.syntax._

    implicit val rulesReads: Reads[Rules] = (
        (JsPath \ "centerId").read[Long] and
        (JsPath \ "ruleList").read[Seq[Rule]]
    )(Rules.apply _)
    implicit val rulesWrites: Writes[Rules] = (
        (JsPath \ "centerId").write[Long] and
        ???
    )(unlift(Rules.unapply))
    implicit val format: Format[Rules] = Format(rulesReads, rulesWrites)
}

This leaves me with two problems.

The first is that if I plug in the expressions I feel are correct in Rule.ruleReads for the two instances of ???, Json.reads[NoValueRule[R]] and Json.reads[ValueRule[R, V]] respectively I get the following compile error

cmd16.sc:8: type mismatch;
 found   : play.api.libs.json.JsResult[Helper.this.OnlyDuringWorkHours]
 required: play.api.libs.json.JsResult[Helper.this.NoValueRule[R]]
        val nvr = Json.reads[NoValueRule[R]]
                            ^cmd16.sc:11: type mismatch;
 found   : play.api.libs.json.JsResult[Helper.this.MaxLeadTime]
 required: play.api.libs.json.JsResult[Helper.this.ValueRule[R,V]]
            val vr = Json.reads[ValueRule[R, V]]
                               ^

the second is if I leave the ??? so that that portion compiles it then fails to compile the rules object with

cmd17.sc:71: No Json deserializer found for type Seq[cmd17Wrapper.this.cmd16.wrapper.Rule]. Try to implement an implicit Reads or Format for this type.
        (JsPath \ "ruleList").read[Seq[Rule]]
                                  ^

I can make the Rules reads / writes a Format instead and get a very similar error

I think the problem with 2 is the difference between Rules containing a Seq[Rule[_]] and me defining an implicit read that should cover any specific rule but not a rule that could be anything

Any ideas how I can get this working? I feel like this should be possible but maybe it isn't.


Solution

  • Although I think you should try some macro-based library that can be found by googling for "play json sealed trait" such as Play JSON Derived Codecs , here is a hand-written solution that might work for you:

    object PlayJson {
    
      import play.api.libs.json._
    
      // fake types instead of your real ones
      type ResStart = Int
      type ResEnd = Int
      type Center = Int
    
      sealed trait Rule[A] {
        def roomId: Option[Long] = None
    
        def valid(in: A): Boolean
      }
    
      sealed trait ValueRule[A, B] extends Rule[A] {
        def value: B
      }
    
      sealed trait NoValueRule[A] extends Rule[A]
    
      case class OnlyDuringWorkHours(override val roomId: Option[Long] = None) extends NoValueRule[((ResStart, ResEnd), Center)] {
        override def valid(in: ((ResStart, ResEnd), Center)): Boolean = true
      }
    
      case class MaxLeadTime(override val roomId: Option[Long] = None, override val value: Int) extends ValueRule[ResStart, Int] {
        override def valid(in: ResStart): Boolean = true
      }
    
      case class MaxDuration(override val roomId: Option[Long] = None, override val value: String) extends ValueRule[(ResStart, ResEnd), String] {
        override def valid(in: (ResStart, ResEnd)): Boolean = true
      }
    
      case class Rules(centerId: Long, ruleList: Seq[Rule[_]])
    
    
      object CompoundFormat {
        final val discriminatorKey = "$type$"
    
        private case class UnsafeFormatWrapper[U, R <: U : ClassTag](format: OFormat[R]) extends OFormat[U] {
          def typeName: String = {
            val clazz = implicitly[ClassTag[R]].runtimeClass
            try {
              clazz.getSimpleName
            }
            catch {
              // getSimpleName might fail for inner classes because of the name mangling
              case _: InternalError => clazz.getName
            }
          }
    
          override def reads(json: JsValue): JsResult[U] = format.reads(json)
    
          override def writes(o: U): JsObject = {
            val base = format.writes(o.asInstanceOf[R])
            base + (discriminatorKey, JsString(typeName))
          }
        }
    
      }
    
      class CompoundFormat[A]() extends OFormat[A] {
    
        import CompoundFormat._
    
        private val innerFormatsByName = mutable.Map.empty[String, UnsafeFormatWrapper[A, _]]
        private val innerFormatsByClass = mutable.Map.empty[Class[_], UnsafeFormatWrapper[A, _]]
    
        override def reads(json: JsValue): JsResult[A] = {
          val jsObject = json.asInstanceOf[JsObject]
          val name = jsObject(discriminatorKey).asInstanceOf[JsString].value
          val innerFormat = innerFormatsByName.getOrElse(name, throw new RuntimeException(s"Unknown child type $name"))
          innerFormat.reads(jsObject)
        }
    
        override def writes(o: A): JsObject = {
          val innerFormat = innerFormatsByClass.getOrElse(o.getClass, throw new RuntimeException(s"Unknown child type ${o.getClass}"))
          innerFormat.writes(o)
        }
    
        def addSubType[R <: A : ClassTag](format: OFormat[R]): Unit = {
          val wrapper = new UnsafeFormatWrapper[A, R](format)
          innerFormatsByName.put(wrapper.typeName, wrapper)
          innerFormatsByClass.put(implicitly[ClassTag[R]].runtimeClass, wrapper)
        }
      }
    
      def buildRuleFormat: OFormat[Rule[_]] = {
        val compoundFormat = new CompoundFormat[Rule[_]]
        compoundFormat.addSubType(Json.format[OnlyDuringWorkHours])
        compoundFormat.addSubType(Json.format[MaxLeadTime])
        compoundFormat.addSubType(Json.format[MaxDuration])
        compoundFormat
      }
    
      def test(): Unit = {
        implicit val ruleFormat = buildRuleFormat
        implicit val rulesFormat = Json.format[Rules]
    
        val rules0 = Rules(1, List(
          OnlyDuringWorkHours(Some(1)),
          MaxLeadTime(Some(2), 2),
          MaxDuration(Some(3), "Abc")
        ))
    
        val json = Json.toJsObject(rules0)
        println(s"encoded: '$json'")
        val rulesDecoded = Json.fromJson[Rules](json)
        println(s"decoded: $rulesDecoded")
      }
    }
    

    calling PlayJson.test prints

    encoded: '{"centerId":1,"ruleList":[{"roomId":1,"$type$":"OnlyDuringWorkHours"},{"roomId":2,"value":2,"$type$":"MaxLeadTime"},{"roomId":3,"value":"Abc","$type$":"MaxDuration"}]}'


    decoded: JsSuccess(Rules(1,List(OnlyDuringWorkHours(Some(1)), MaxLeadTime(Some(2),2), MaxDuration(Some(3),Abc))),)

    The main idea is to have CompoundFormat for the sealed trait that stores mapping between the class name and the corresponding OFormat for each child.


    Update (about reflection concerns)

    Here is a non-generic version of CompoundFormat that I expect to be similar to what a macro-based library can generate (actually I expect good macro-based library also handle a case when some of the children of the sealed trait are singleton object rather than class which this code does not handle):

    object ExplicitRuleFormat {
      implicit val format: OFormat[Rule[_]] = new ExplicitRuleFormat()
    
      private object InnerFormats {
    
        final val discriminatorKey = "$type$"
        implicit val onlyDuringWorkHoursFormat = Json.format[OnlyDuringWorkHours]
        final val onlyDuringWorkHoursTypeName = "OnlyDuringWorkHours"
        implicit val maxLeadTimeFormat = Json.format[MaxLeadTime]
        final val maxLeadTimeTypeName = "MaxLeadTime"
        implicit val maxDurationFormat = Json.format[MaxDuration]
        final val maxDurationTypeName = "MaxDuration"
      }
    
    }
    
    class ExplicitRuleFormat extends OFormat[Rule[_]] {
    
      import ExplicitRuleFormat.InnerFormats._
    
      override def reads(json: JsValue): JsResult[Rule[_]] = {
        val jsObject = json.asInstanceOf[JsObject]
        val name = jsObject(discriminatorKey).asInstanceOf[JsString].value
        name match {
          case s if onlyDuringWorkHoursTypeName.equals(s) => Json.fromJson[OnlyDuringWorkHours](jsObject)
          case s if maxLeadTimeTypeName.equals(s) => Json.fromJson[MaxLeadTime](jsObject)
          case s if maxDurationTypeName.equals(s) => Json.fromJson[MaxDuration](jsObject)
        }
      }
    
      override def writes(r: Rule[_]): JsObject = r match {
        case rr: OnlyDuringWorkHours => writeImpl(rr, onlyDuringWorkHoursTypeName)
        case rr: MaxLeadTime => writeImpl(rr, maxLeadTimeTypeName)
        case rr: MaxDuration => writeImpl(rr, maxDurationTypeName)
      }
    
      def writeImpl[R <: Rule[_]](r: R, typeName: String)(implicit w: OWrites[R]): JsObject = {
        Json.toJsObject(r) + (discriminatorKey, JsString(typeName))
      }
    }
    

    and with that test becomes:

    def test(): Unit = {
      import ExplicitRuleFormat.format
      implicit val rulesFormat = Json.format[Rules]
    
      val rules0 = Rules(1, List(
        OnlyDuringWorkHours(Some(1)),
        MaxLeadTime(Some(2), 2),
        MaxDuration(Some(3), "Abc")
      ))
    
      val json = Json.toJsObject(rules0)
      println(s"encoded: '$json'")
      val rulesDecoded = Json.fromJson[Rules](json)
      println(s"decoded: $rulesDecoded")
    }
    

    Effectively you just replace implicit val ruleFormat = buildRuleFormat with import ExplicitRuleFormat.format.