Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making Reads and Writes in Scala Play for lists of custom classes

So I have two classes in my project

case class Item(id: Int, name: String)

and

case class Order(id: Int, items: List[Item])

I'm trying to make reads and writes properties for Order but I get a compiler error saying:

"No unapply or unapplySeq function found"

In my controller I have the following:

implicit val itemReads = Json.reads[Item]
implicit val itemWrites = Json.writes[Item]
implicit val listItemReads = Json.reads[List[Item]]
implicit val listItemWrites = Json.writes[List[Item]]

The code works for itemReads and itemWrites but not for the bottom two. Can anyone tell me where I'm going wrong, I'm new to Play framework.

Thank you for your time.

like image 595
Will Avatar asked Dec 23 '15 14:12

Will


3 Answers

The "No unapply or unapplySeq function found" error is caused by these two:

implicit val listItemReads = Json.reads[List[Item]]
implicit val listItemWrites = Json.writes[List[Item]]

Just throw them away. As Ende said, Play knows how to deal with lists.

But you need Reads and Writes for Order too! And since you do both reading and writing, it's simplest to define a Format, a mix of the Reads and Writes traits. This should work:

case class Item(id: Int, name: String)

object Item {
  implicit val format = Json.format[Item]
}

case class Order(id: Int, items: List[Item])

object Order {
  implicit val format = Json.format[Order]
}

Above, the ordering is significant; Item and the companion object must come before Order.

So, once you have all the implicit converters needed, the key is to make them properly visible in the controllers. The above is one solution, but there are other ways, as I learned after trying to do something similar.

like image 117
Jonik Avatar answered Oct 20 '22 00:10

Jonik


You don't actually need to define those two implicits, play already knows how to deal with a list:

scala> import play.api.libs.json._ 
import play.api.libs.json._

scala>   case class Item(id: Int, name: String)
defined class Item

scala>   case class Order(id: Int, items: List[Item])
defined class Order

scala>   implicit val itemReads = Json.reads[Item]
itemReads: play.api.libs.json.Reads[Item] = play.api.libs.json.Reads$$anon$8@478fdbc9

scala>   implicit val itemWrites = Json.writes[Item]
itemWrites: play.api.libs.json.OWrites[Item] = play.api.libs.json.OWrites$$anon$2@26de09b8

scala>   Json.toJson(List(Item(1, ""), Item(2, "")))
res0: play.api.libs.json.JsValue = [{"id":1,"name":""},{"id":2,"name":""}]

scala>   Json.toJson(Order(10, List(Item(1, ""), Item(2, ""))))
res1: play.api.libs.json.JsValue = {"id":10,"items":[{"id":1,"name":""},{"id":2,"name":""}]}

The error you see probably happens because play uses the unapply method to construct the macro expansion for your read/write and List is an abstract class, play-json needs concrete type to make the macro work.

like image 28
Ende Neu Avatar answered Oct 19 '22 23:10

Ende Neu


This works:

case class Item(id: Int, name: String)

case class Order(id: Int, items: List[Item])

implicit val itemFormat = Json.format[Item]
implicit val orderFormat: Format[Order] = (
  (JsPath \ "id").format[Int] and
    (JsPath \ "items").format[JsArray].inmap(
      (v: JsArray) => v.value.map(v => v.as[Item]).toList,
      (l: List[Item]) => JsArray(l.map(item => Json.toJson(item)))
    )
  )(Order.apply, unlift(Order.unapply))

This also allows you to customize the naming for your JSON object. Below is an example of the serialization in action.

Json.toJson(Order(1, List(Item(2, "Item 2"))))
res0: play.api.libs.json.JsValue = {"id":1,"items":[{"id":2,"name":"Item 2"}]}

Json.parse(
  """
    |{"id":1,"items":[{"id":2,"name":"Item 2"}]}
  """.stripMargin).as[Order]
res1: Order = Order(1,List(Item(2,Item 2)))

I'd also recommend using format instead of read and write if you are doing symmetrical serialization / deserialization.

like image 22
nattyddubbs Avatar answered Oct 20 '22 00:10

nattyddubbs