14
votes

I'm trying to map a Scala case class to JSON using Play 2.x. This works for simple versions of the case class, but not when there's a Seq or List of objects involved: then I get 'no implicit format' and 'no unapply function found' errors.

The code I'm using for this is the following:

case class Book(title: String, authors: Seq[Author])
case class Author(name: String)

I've used the Json.format macro to generate the Reads and Writes for this:

implicit val bookFormat = Json.format[Book]
implicit val authorFormat = Json.format[Author]

But now when I'm compiling my code, I get the following error:

Error:(25, 40) Play 2 Compiler: 
 /Users/erikp/Userfiles/projects/play/booksearch/app/models/user.scala:25: No implicit format for Seq[models.Author] available.
   implicit val bookFormat = Json.format[Book]
                                        ^

Without the Seq it works nicely, but with the Seq, it fails. I tried adding implicit val authorsFormat = Json.format[Seq[Author]] to the implicit converters, but that has no effect.

1
Defining them in the reversed order should work: implicit val authorFormat = Json.format[Author] implicit val bookFormat = Json.format[Book]Bobby
Wow, that I missed that. Thanks! Btw, you also have a suggestion to keep this list from expanding? Do I really have to register a Json.format for each case class manually?Erik Pragt

1 Answers

11
votes

Define the formatters respecting their dependency order, for each class in the graph that needs to be serialized.

Formatting Book requires formatting Author, so define the Author formatter before the Book formatter.

For example, with this Models.scala file:

package models

import play.api.libs.json._

case class Book(title: String, authors: Seq[Author])
case class Author(name: String)

object Formatters {
  implicit val authorFormat = Json.format[Author]
  implicit val bookFormat = Json.format[Book]
}

and this JsonExample.scala file:

package controllers

import models._
import models.Formatters._
import play.api.mvc._
import play.api.libs.json._

object JsonExample extends Controller {

  def listBooks = Action {
    val books = Seq(
      Book("Book One", Seq(Author("Author One"))),
      Book("Book Two", Seq(Author("Author One"), Author("Author Two")))
    )
    val json = Json.toJson(books)
    Ok(json)
  }

}

a request to listBooks will produce this result:

< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Content-Length: 133
<     
[{"title":"Book One","authors":[{"name":"Author One"}]},{"title":"Book Two","authors":[{"name":"Author One"},{"name":"Author Two"}]}]

For more advanced formatting, including partial serialization to avoid having to declare formatters for classes that should not be serialized, see JSON Reads/Writes/Format Combinators.

It should be kept in mind that the classes to be serialized don't necessarily have to be the domain model classes. It may be helpful to declare data transfer object (DTO) classes that reflect the desired JSON structure, and instantiate them from the domain model. This way, serialization is straightforward with Json.format and there isn't the issue of partial serialization, with the added benefit of a typesafe representation of the JSON API.

For example, this BookDTO.scala file defines a BookDTO data transfer object that uses only types that can be serialized to JSON without requiring further definition:

package dtos

import models._
import play.api.libs.json.Json

case class BookDTO (title: String, authors: Seq[String])

object BookDTO {

  def fromBook(b: Book) = BookDTO(b.title, b.authors.map(_.name))

  implicit val bookDTOFormat = Json.format[BookDTO]

}

and this JsonExample2.scala file shows how to use this pattern:

package controllers

import dtos._
import dtos.BookDTO._
import models._
import play.api.mvc._
import play.api.libs.json._
import play.api.libs.functional.syntax._

object JsonExample2 extends Controller {

  def listBooks = Action {
    val books = Seq(
      Book("Book One", Seq(Author("Author One"))),
      Book("Book Two", Seq(Author("Author One"), Author("Author Two")))
    )
    val booksDTO = books.map(BookDTO.fromBook(_))
    Ok(Json.toJson(booksDTO))
  }

}