Parsing Json with the Play Framework
This article discusses different common scenarios for JSON parsing and conversion, useful when working with the Play Framework library. All the examples in the article, use one or more of the following library imports:
1import play.api.libs.functional.syntax._
2import play.api.libs.json.{Format, JsError, JsNull, JsPath, JsResult, JsString, JsSuccess, JsValue, Json, OFormat, Reads}
The first import is required to use the special and
and or
functions found through the examples below.
JSON that matches property names
The first snippet allows us to parse different basic data types from a json-formatted input. The following json:
1{
2 "string": "some value",
3 "long": 1,
4 "double": 2.0,
5 "boolean": true,
6 "date": "2022-01-03T16:21:23.377Z",
7 "strings": ["one", "two", "three"],
8 "longs": [1, 2, 3]
9}
can be parsed by the following case class and its companion object:
1case class MyClass (
2 string: String,
3 long: Long,
4 double: Double,
5 boolean: Boolean,
6 date: Instant,
7 strings: List[String],
8 longs: List[Long]
9)
10
11object MyClass {
12 implicit val format: OFormat[MyClass] = Json.format[MyClass]
13}
The implicit
format
value in the MyClass
object deals with the magic behind the scenes. It automatically parses from and converts to json values.
This is all that is required for basic conversion between json and case classes. The sections below keep adding features and exploring special parsing and conversion cases.
JSON with optional fields
Sometimes json values are not present. We use Scala’s Option
to represent these values in case classes. See the following example:
1case class MyClass (
2 string: Option[String],
3 number: Option[Long]
4)
5
6object MyClass {
7 implicit val format: OFormat[MyClass] = Json.format[MyClass]
8}
The Option
fields allow us to parse a json input with a missing numeric field:
1{
2 "string": "some value",
3}
or a missing string field:
1{
2 "number": 1,
3}
or an input that contains both values:
1{
2 "string": "some value",
3 "number": 1
4}
even an empty json object is valid, because all the fields declared in our case class are optional:
1{ }
The scala fields will be set to None
for those scenarios where the field is missing from the json source. A similar pattern can be used for reading optional lists. For example:
1final case class MyClass (
2 strings: Option[List[String]],
3 numbers: Option[List[Long]]
4)
5
6object MyClass {
7 implicit val format: OFormat[MyClass] = Json.format[MyClass]
8}
Separating format into reader and writer
We used OFormat
in the previous examples for reading and writing json values. There are times when we need to separate the reading and the writing parts. We can use the Reads
and Writes
to do it like this:
1case class MyClass(
2 string: String,
3 number: Long
4)
5
6object MyClass {
7
8 implicit val reads: Reads[MyClass] = Json.reads[MyClass]
9 implicit val writes: Writes[MyClass] = Json.writes[MyClass]
10
11}
It is not necessary to have both a reader and a writer. We can only define the one that we need. For example, if we are reading json, but not writing it, then a reader will be enough.
This last example accomplishes the same as OFormat
and it seems that at this point we have not gained anything, but its real value will be seen in the following sections.
Custom Reads
A custom read can be used when json’s field names do not match the ones we have defined for our case class.
1case class User (
2 id: String,
3 name: String,
4 colors: List[String],
5 flavors: Option[List[String]]
6)
7
8object User {
9 implicit val reads: Reads[User] = (
10 (JsPath \ "user_id").read[String] and
11 (JsPath \ "user_name").read[String] and
12 (JsPath \ "favorite_colors").read[List[String]] and
13 (JsPath \ "favorite_flavors").readNullable[List[String]]
14 ) (User.apply _)
15}
The reads
value has a series of read
and readNullable
calls that follow the field order defined by the case class. This does not mean that the json file has to match this specific order, it only means we have to follow the case class order when we place the series of read
's and readNullable
's in our code.
read
is used for non-optional values and readNullable
for optional ones.
Note how each of the read
/readNullable
calls specifies the type to be read (String
, List[String]
, etc.)
Custom Writes
Similar to custom reads, there may be times when we need to generate json that writes to a specific format.
1case class Customer (
2 id: String,
3 name: String,
4 colors: Option[List[String]],
5 recurrent: Option[Boolean]
6)
7
8object Customer {
9
10 implicit val writes: Writes[Customer] = (
11 (JsPath \ "customer_id").write[String] and
12 (JsPath \ "customer_name").write[String] and
13 (JsPath \ "favorite_colors").writeNullable[List[String]] and
14 (JsPath \ "recurrent_customer").writeNullable[Boolean]
15 )(unlift(Customer.unapply))
16
17}
The writes
value has a series of write
and writeNullable
calls that follow the field order defined by the case class.
write
is used for non-optional values while writeNullable
for optional ones.
Note how the write
and writeNullable
calls declare the type to be written (String
, List[String]
, Boolean
, etc.)
Special case: parsing to case classes with one property
The following pattern must be followed when we have a case class with a single property:
1case class User (
2 name: Option[String]
3)
4
5object User {
6
7 implicit val read: Reads[User] =
8 (JsPath \ "some_name_field").readNullable[String]
9 .map { s => User(s) }
10
11}
Note that we are using map
instead of apply
. The following can be used an alternative implementation:
1object User {
2
3 implicit val reads: Reads[User] =
4 (JsPath \ "some_name_field").readNullable[String].map(User.apply)
5
6}
Play’s json library takes care of this required change when we are not using a custom OFormat
/Reads
/Writes
, as we can see in the following example:
1case class User (
2 name: String
3)
4
5
6object User {
7
8 implicit val reads: Reads[User] = Json.reads[User]
9 implicit val writes: Writes[User] = Json.writes[User]
10 // or just:
11 // implicit val format: OFormat[User] = Json.format[User]
12
13}
Fields that can return different data types
There are times when a json field can have multiple data types. The following example shows how to deal with a field that sometimes is received as a String
and some others as a List[String]
.
In our example, the field audience
can be received as a single string or as a list, so in our case class we have decided to declare it as a List[String]
. This is useful because we can represent a single string as a list with one item.
1case class Presentation (
2 subject: String,
3 audience: List[String]
4)
5
6object Presentation {
7
8 val stringAsListOfString: Reads[List[String]] =
9 implicitly[Reads[String]].map(List(_))
10
11 implicit val reads: Reads[Presentation] = (
12 (JsPath \ "sub").read[String] and
13 ((JsPath \ "aud").read[List[String]] or (JsPath \ "aud").read[List[String]](readStringAsListOfString))
14 ) (Presentation.apply _)
15
16}
stringAsListOfString
is a helper function that reads a single string and maps it to a list with one item.
The reads
value tries to parse aud
as a List[String]
. If it fails, the code after or
will be used. This is where we call our stringAsListOfString
function. Do not miss the right use of parenthesis when using or
.
Note the way we call stringAsListOfString
. We will use that same syntax in the following section.
Field validation
Field validation uses a syntax similar to the one found in the previous section. Play’s JSON library includes some predefined validators, but we can create our own if we need some specific functionality.
1case class MyClass (
2 l: Long,
3 d: Double,
4 email: String,
5 custom1: String,
6 custom2: String
7)
8
9object MyClass {
10 implicit val reads: Reads[MyClass] = (
11 (JsPath \ "l").read[Long](Reads.min(10) or Reads.max(20)) and
12 (JsPath \ "d").read[Double](Reads.min(0.1) or Reads.max(1.0)) and
13 (JsPath \ "email").read[String](Reads.email) and
14 (JsPath \ "custom1").read[String](Reads.pattern("""^[a-zA-Z0-9]{5,10}]$""".r, "Some generic error"))
15 (JsPath \ "custom2").read[String](Reads.pattern("""^[a-zA-Z0-9]{5,10}]$""".r, "error.custom"))
16 ) (MyClass.apply _)
17}
Remember that we can use readNullable
instead of read
when we have optional fields.
custom1
and custom2
fields use regular expressions for validation. The second parameter is the error message to use if the validation fails. Note that for custom1
we are using a generic string as error message, and for custom2
we are leveraging on Play’s internationalization functionality to display the appropriate message.
Field transformation
Where are times when Play needs some help to convert between types. A json input may contain “TRUE”, “true” or “True” as valid Boolean values. We have to create a function that reads the value and converts it to our target data type.
1
2case class Presentation (
3 subject: String,
4 hidden: Boolean
5)
6
7object Presentation {
8
9 val stringAsBoolean: Reads[Boolean] =
10 implicitly[Reads[String]].map(_.toLowerCase == "true")
11
12 implicit val reads: Reads[Presentation] = (
13 (JsPath \ "sub").read[String] and
14 (JsPath \ "is_hidden").read[Boolean](stringAsBoolean)
15 ) (Presentation.apply _)
16
17}
We are adding a function called stringAsBoolean
which reads a string and then maps the read value to the lower-case “true” string.
Converter functions (also called Formatters) can live in external objects as well. In the following example we read a string and convert it to a URI that matches the required data type in our case class.
Important: In the following example we are using the
Format
trait instead of theOFormat
one.
OFormat
does not work for this implementation, howeverFormat
may work for any of the previous examples found on this page.
1object JsonFormatters {
2
3 import java.net.URI
4
5 implicit val uriFormat: Format[URI] = new Format[URI] {
6 def writes(s: URI): JsValue = JsString(s.toString)
7
8 def reads(json: JsValue): JsResult[URI] = json match {
9 case JsNull => JsError()
10 case _ => JsSuccess(URI.create(json.as[String]))
11 }
12 }
13
14}
As in previous examples, writes
is only required if we want to be able to generate json from our case classes.
1case class Link (
2 id: String,
3 url: URI,
4)
5
6object Link {
7 implicit val uriFormat: Format[URI] = JsonFormatters.uriFormat
8 implicit val format = Json.format[Link]
9}
We can mix everything we have learned before and create a custom reader as well:
1object Link {
2 implicit val reads: Reads[Link] = (
3 (JsPath \ "link_id").read[String] and
4 (JsPath \ "link_url").read[URI](JsonFormatters.uriFormat)
5 ) (Link.apply _)
6}
Splitting case classes
Some times it is necessary to split a json input parsing into multiple sections. This is required when we are doing parsing of more than 22 fields – remember case classes cannot have more than 22 fields – or for code organization purposes.
1case class Claims (
2 registered: Registered,
3 public: List[Public]
4)
5
6object Claims {
7
8 implicit val reads: Reads[Claims] = (
9 JsPath.read[Registered] and
10 (JsPath \ "data").read[List[Public]]
11 ) (Claims.apply _)
12
13}
14
15case class Registered (
16 subject: String,
17 audience: List[String]
18)
19
20object Registered {
21 implicit val reads: Reads[Registered] = (
22 (JsPath \ "sub").read[String] and
23 (JsPath \ "aud").read[List[String]])
24 ) (Registered.apply _)
25}
26
27case class Public (
28 name: String,
29 tags: List[String]
30)
31
32object Public {
33 implicit val reads: Reads[Public] = (
34 (JsPath \ "name").read[String] and
35 (JsPath \ "tags").readNullable[List[String]])
36 ) (Public.apply _)
37 // or simply
38 // implicit val reads: Reads[Public] = Json.reads[Public]
39}
The previous code should be able to parse the following json input:
1{
2 "sub": "some string",
3 "aud": ["some audience 1", "some audience 2"],
4 "data": [
5 {
6 "name": "some string 1",
7 "tags": ["tag 1", "tag 2"]
8 },
9 {
10 "name": "some string 2",
11 "tags": ["tag 3", "tag 4"]
12 }
13 ]
14}