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.
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
.