Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting a Heterogeneous List to and from Json in Play for Scala

I am trying to take a list of items that share a common trait and covert them to and from Json. The example I have presented here is of a train with its engine(s) and cars. There are three classes to create a train: Engines, Passenger Cars and Freight Cars. (I thought a simple reality based example would be easiest to understand, it is also much less complicated then the problem I am trying to solve.)

The parts of the train are defined as following:

package models

sealed trait Vehicle {
  val kind: String
  val maxSpeed: Int = 0
  def load: Int
}

case class Engine(override val maxSpeed: Int, val kind: String, 
  val power: Float, val range: Int) extends Vehicle {
  override val load: Int = 0
}

case class FreightCar(override val maxSpeed: Int, val kind: String, 
  val load: Int) extends Vehicle {}

case class PassengerCar(override val maxSpeed: Int, val kind: String, 
  val passengerCount: Int) extends Vehicle {
  override def load: Int = passengerCount * 80
}

(file Vehicle.scala)

The train is defined as:

package models

import scala.collection.mutable
import play.api.Logger
import play.api.libs.json._

case class Train(val name: String, val cars: List[Vehicle]) {
  def totalLoad: Int = cars.map(_.load).sum
  def maxSpeed: Int = cars.map(_.maxSpeed).min
}

object Train {
   def save(train: Train) {
   Logger.info("Train saved ~ Name: " + train.name)
  }
}

(file Train.scala)

As you can see a train is created by adding 'cars' to the list stored in Train.

My problem arrises when converting my train to Json or trying to read it from Json. Here is my current code for doing this:

package controllers

import play.api.mvc._
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.data.validation.ValidationError
import play.api.libs.functional.syntax._
import models.Vehicle
import models.Engine
import models.FreightCar
import models.PassengerCar
import models.Train

class Trains extends Controller {

implicit val JsPathWrites = Writes[JsPath](p => JsString(p.toString))

implicit val ValidationErrorWrites =
  Writes[ValidationError](e => JsString(e.message))

 implicit val jsonValidateErrorWrites = (
  (JsPath \ "path").write[JsPath] and
  (JsPath \ "errors").write[Seq[ValidationError]]
  tupled
)

implicit object engineLoadWrites extends Writes[Engine] {
  def writes(e: Engine) = Json.obj(
    "maxSpeed" -> Json.toJson(e.maxSpeed),
    "kind" -> Json.toJson(e.kind),
    "power" -> Json.toJson(e.power),
    "range" -> Json.toJson(e.range)
  )
}

implicit object freightCarLoadWrites extends Writes[FreightCar] {
  def writes(fc: FreightCar) = Json.obj(
    "maxSpeed" -> Json.toJson(fc.maxSpeed),
    "kind" -> Json.toJson(fc.kind),
    "load" -> Json.toJson(fc.load)
  )
}

implicit object passengerCarLoadWrites extends Writes[PassengerCar] {
  def writes(pc: PassengerCar) = Json.obj(
    "maxSpeed" -> Json.toJson(pc.maxSpeed),
    "kind" -> Json.toJson(pc.kind),
    "passengerCount" -> Json.toJson(pc.passengerCount)
  )
}

implicit object trainWrites extends Writes[Train] {
  def writes(t: Train) = Json.obj(
    "name" -> Json.toJson(t.name),
    "cars" -> Json.toJson(t.cars)   // Definitely not correct!
  )
}

/* --- Writes above, Reads below --- */

implicit val engineReads: Reads[Engine] = (
  (JsPath \ "maxSpeed").read[Int] and
  (JsPath \ "kind").read[String] and
  (JsPath \ "power").read[Float] and
  (JsPath \ "range").read[Int]
)(Engine.apply _)

implicit val freightCarReads: Reads[FreightCar] = (
  (JsPath \ "maxSpeed").read[Int] and
  (JsPath \ "kind").read[String] and
  (JsPath \ "load").read[Int]
)(FreightCar.apply _)

implicit val passengerCarReads: Reads[PassengerCar] = (
  (JsPath \ "maxSpeed").read[Int] and
  (JsPath \ "kind").read[String] and
  (JsPath \ "passengerCount").read[Int]
)(PassengerCar.apply _)

implicit val joistReads: Reads[Train] = (
  (JsPath \ "name").read[String](minLength[String](2)) and
  (JsPath \ "cars").read[List[Cars]]  // Definitely not correct!
)(Train.apply _)

/**
* Validates a JSON representation of a Train.
*/
  def save = Action(parse.json) { implicit request =>
    val json = request.body
    json.validate[Train].fold(
      valid = { train =>
        Train.save(train)
        Ok("Saved")
      },
      invalid = {
        errors => BadRequest(Json.toJson(errors))
      }
    )
  }
}

(File Trains.scala)

All the code to create and consume Json for Lists of FreightCars, Engines works but I can not create the Json to handle all three types at once, ex something like this:

implicit object trainWrites extends Writes[Train] {
   def writes(t: Train) = Json.obj(
     "name" -> Json.toJson(t.name),
     "cars" -> Json.toJson(t.cars)
   )
 }

Json.toJson for the List simply doesn't work; nor does its read counterpart. When I replace t.cars in the code above with class Engines or any of my single concrete classes everything works.

How might I elegantly solve this to make my Json readers and Writers work? Or alternatively, if Scala Play's Json encoder decoder is not a good choice for such a task is there a more suitable Json library?

like image 749
BGW Avatar asked Oct 30 '22 09:10

BGW


1 Answers

Running your Writes code returns the following error (which gives a clue on what to fix):

Error:(78, 27) No Json deserializer found for type Seq[A$A90.this.Vehicle]. Try to implement an implicit Writes or Format for this type.
"cars" -> Json.toJson(t.cars)   // Definitely not correct!
                     ^

There is no deserializer found for Vehicle, so you need to add a Reads/Writes (or Format) for Vehicle. This will just delegate down to the actual Format for the type.

The implementation is pretty straightforward for the writes, one can just pattern match against the type. For the reads I'm looking for a distinguishing property in the json to give an indication on what Reads to delegate to.

Note that play-json provides helpers so you don't have to manually implement Writes/Reads for case classes, so you can write val engineLoadWrites : Writes[Engine] = Json.writes[Engine]. This is used in the sample below.

//Question code above, then ...

val engineFormat = Json.format[Engine]
val freightCarFormat = Json.format[FreightCar]
val passengerCarFormat = Json.format[PassengerCar]

implicit val vehicleFormat = new Format[Vehicle]{
  override def writes(o: Vehicle): JsValue = {
    o match {
      case e : Engine => engineFormat.writes(e)
      case fc : FreightCar => freightCarFormat.writes(fc)
      case pc : PassengerCar => passengerCarFormat.writes(pc)
    }
  }

  override def reads(json: JsValue): JsResult[Vehicle] = {
    (json \ "power").asOpt[Int].map{ _ =>
      engineFormat.reads(json)
    }.orElse{
      (json \ "passengerCount").asOpt[Int].map{ _ =>
        passengerCarFormat.reads(json)
      }
    }.getOrElse{
      //fallback to FreightCar
      freightCarFormat.reads(json)
    }
  }
}


implicit val trainFormat = Json.format[Train]


val myTrain = Train(
  "test",
  List(
    Engine(100, "e-1", 1.0.toFloat, 100),
    FreightCar(100, "f-1", 20),
    PassengerCar(100, "pc", 10)
  )
)

val myTrainJson = trainFormat.writes(myTrain) 
/** => myTrainJson: play.api.libs.json.JsObject = {"name":"test","cars":[{"maxSpeed":100,"kind":"e-1","power":1.0,"range":100},{"maxSpeed":100,"kind":"f-1","load":20},{"maxSpeed":100,"kind":"pc","passengerCount":10}]} */

val myTrainTwo = myTrainJson.as[Train]
/* => myTrainTwo: Train = Train(test,List(Engine(100,e-1,1.0,100), FreightCar(100,f-1,20), PassengerCar(100,pc,10))) */
like image 68
ed. Avatar answered Nov 15 '22 06:11

ed.