Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you're building APIs in Scala, you'll spend a huge amount of your time moving data between your strongly-typed case classes and the loosely-typed world of JSON. While there are several libraries out there, play-json is a staple because it strikes a great balance between automation and explicit control. I've always preferred it over some of the more "magical" libraries because when something goes wrong with your data mapping, the compiler usually tells you exactly why.

Defining our Book domain

Let's build a simple inventory system for a bookstore. I'll start by defining a Book case class. We need a few different types here—strings, a double for the price, and a list of tags—just to see how the library handles collections.

case class Book(title: String, author: String, price: Double, tags: List[String])

Now, we need a way to tell Scala how to turn this class into JSON (Writing) and how to turn JSON back into this class (Reading). In play-json, we use a Format, which is essentially a combination of Reads and Writes.

Letting the compiler do the heavy lifting

For most simple case classes, you don't want to write the mapping by hand. It's tedious and error-prone. Instead, I'll use the Json.format macro. This generates the boilerplate for us at compile time.

import play.api.libs.json._

implicit val bookFormat: Format[Book] = Json.format[Book]

// Now we can easily convert to JSON
val myBook = Book("The Pragmatic Programmer", "Hunt & Thomas", 45.00, List("Programming", "Career"))
val json = Json.toJson(myBook) 
// Result: {"title":"The Pragmatic Programmer","author":"Hunt & Thomas","price":45.0,"tags":["Programming","Career"]}

By marking bookFormat as implicit, any function that needs to know how to handle a Book (like Json.toJson) will just find this formatter in the scope and use it. It's clean, and it works perfectly as long as your JSON keys match your case class field names exactly.

The "where is my data?" moment

Here is where I usually trip up when I'm moving fast. I tried to parse a JSON string coming from a mock API, and I wrote this:

val jsonString = """{"title": "Scala Guide", "author": "Expert", "price": 20.0, "tags": ["Scala"]}"""
val json = Json.parse(jsonString)
val book = json.as[Book] // I thought this was the way!

The problem? .as[T] is an "unsafe" method. If the JSON is slightly malformed—maybe a field is missing or a type is wrong—it throws a runtime exception. In a production app, crashing your entire request thread because a client sent a string instead of a double is a disaster. I realized I was treating the JSON as a guaranteed success rather than a possibility.

The right way is to use validate, which returns a JsResult. This forces you to handle the failure case explicitly.

val result = json.validate[Book]

result match {
  case JsSuccess(book, _) => println(s"Got the book: ${book.title}")
  case JsError(errors)    => println(s"JSON was invalid: $errors")
}

Customizing the JSON keys

In the real world, you rarely get to dictate the JSON format. You'll often encounter APIs that use snake_case while your Scala code uses camelCase. If the API sends book_title instead of title, Json.format will fail because the names don't match.

To fix this, we stop using the macro and define the Reads and Writes manually. I'll do this by creating a custom Format using the (field) reads` notation.

implicit val customBookFormat: Format[Book] = new Format {
  def reads(json: JsValue): JsResult[Book] = for {
    title  <- (json \ "book_title").as[String]
    author <- (json \ "book_author").as[String]
    price  <- (json \ "book_price").as[Double]
    tags   <- (json \ "book_tags").as[List[String]]
  } yield Book(title, author, price, tags)

  def writes(book: Book): JsValue = Json.obj(
    "book_title"  → book.title,
    "book_author" → book.author,
    "book_price"  → book.price,
    "book_tags"   → book.tags
  )
}

Now we have a robust mapping that decouples our internal Scala naming conventions from the external API's requirements. It's more code, but it's where the actual reliability comes from.




📋 Practical Task

Implementing a Weighted Rating for the Book Inventory

You need to extend the Book inventory system to include a rating system. Follow these requirements:

  • Update the Book case class to include a rating field of type Double.
  • Create a custom Format[Book] that maps the Scala field rating to a JSON key called average_rating.
  • Write a small test script that takes the following JSON string: {"book_title": "Scala Mastery", "book_author": "Jane Doe", "book_price": 30.0, "book_tags": ["Advanced"], "average_rating": 4.8}.
  • Use .validate[Book] to parse the string and print "Successfully parsed" if it works, or the specific errors if it fails.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.