Skip to content

Instantly share code, notes, and snippets.

@salomvary
Last active July 9, 2017 16:58
Show Gist options
  • Save salomvary/2d919da05b23010570e7 to your computer and use it in GitHub Desktop.
Save salomvary/2d919da05b23010570e7 to your computer and use it in GitHub Desktop.
PlayJson Crash Course
import java.time.{Instant, ZonedDateTime}
import play.api.libs.json._
import scala.util.control.NonFatal
/**
* PlayJson Basics
*/
// See type hierarchy (^H) of:
val rootOfAllTheThings = classOf[JsValue]
/**
* Parsing JSON
*/
var beerJson = """
{
"name": "Doppelgänger Stout",
"abv": 0.048,
"calories": 144,
"expires_at": "2015-12-01T13:20:00Z",
"barcode": null
}
"""
val json: JsValue = Json.parse(beerJson)
val jsonObj: JsObject = json.as[JsObject]
// val jsonArr: JsArray = json.as[JsArray]
// JsResultException
/**
* Serializing JSON
*/
val city: JsObject = Json.obj("name" -> "Berlin")
Json.stringify(city)
/**
* Traversing JSON
*/
(json \ "name").as[String]
// This will throw:
//(json \ "barcode").as[String]
(json \ "barcode").as[Option[String]] // None
(json \ "barcode").asOpt[String] // None
// However, be warned, asOpt[T] and as[Option[T]] both swallow mismatching types
(Json.obj("foocode" -> "123") \ "barcode").asOpt[String] // None
(Json.obj("barcode" -> JsNull) \ "barcode").asOpt[String] // None
(Json.obj("barcode" -> 123) \ "barcode").asOpt[String] // None
(Json.obj("foocode" -> "123") \ "barcode").as[Option[String]] // None
(Json.obj("barcode" -> JsNull) \ "barcode").as[Option[String]] // None
(Json.obj("barcode" -> 123) \ "barcode").as[Option[String]] // None
/**
* Programmatically creating JSON
*/
val hightowerIPA: JsObject = Json.obj(
"style" -> "America IPA",
"brewed_by" -> "Hightower Brewery",
"calories" -> 123
)
val tehNumber = JsNumber(2)
/**
* Manipulating JSON
*/
hightowerIPA + ("color" -> JsString("amber"))
hightowerIPA - "calories"
/**
* Instantiating case classes from JSON
*/
var beerJsObj: JsValue = Json.parse("""
{
"name": "Doppelgänger Stout",
"abv": 0.048,
"calories": 144,
"expires_at": "2015-12-01T13:20:00Z",
"barcode": null
}
""")
case class Beer(name: String, abv: Double)
val stout: Beer = beerJsObj.as[Beer]
// Error:(73, 9) No Json deserializer found for type Beer.
// Try to implement an implicit Reads or Format for this type.
// beerJsObj.as[Beer]
// ^
//// Don't forget this import!
import play.api.libs.functional.syntax._
implicit val beerReads: Reads[Beer] = (
(JsPath \ "name").read[String] and
(JsPath \ "abv").read[Double]
)(Beer.apply _)
// WTF Implicits?
beerJsObj.as[Beer](beerReads) == beerJsObj.as[Beer]
/**
* Only one field?
*/
case class Beer(name: String)
val beerReads: Reads[Beer] = (JsPath \ "name").read[String].map(Beer)
val beerWrites: Writes[Beer] = (JsPath \ "name").write[String].contramap(unlift(Beer.unapply))
val beerFormat: Format[Beer] = (JsPath \ "name").format[String].inmap(Beer, unlift(Beer.unapply))
/**
* Creating JSON from case classes
*/
val zwickelbier: JsValue = Json.toJson(Beer("Plum Zwickelbier", .08))
//Error:(57, 13) No Json serializer found for type Beer.
// Try to implement an implicit Writes or Format for this type.
//Json.toJson(Beer("Plum Zwickelbier", .08))
//^
implicit val beerWrites: Writes[Beer] = (
(JsPath \ "name").write[String] and
(JsPath \ "abv").write[Double]
)(unlift(Beer.unapply)) // *
// * "an extractor extracts the parameters from which an object passed to it was created"
// Without using implicits:
val someFineZwickelbier = Beer("Plum Zwickelbier", .08)
Json.toJson(someFineZwickelbier) ==
beerWrites.writes(someFineZwickelbier)
/**
* Reading and writing case classes with optional fields
*/
case class Beer(name: String, barcode: Option[String])
// Reading with read[Option[T]]
implicit val beerReads: Reads[Beer] = (
(JsPath \ "name").read[String] and
// Prefer readNullable[T] to read[Option[T]] because the behavior is less weird, see below
(JsPath \ "barcode").read[Option[String]]
)(Beer.apply _)
Json.obj("name" -> "Plum Zwickelbier", "barcode" -> JsNull).as[Beer]
// Beer(Plum Zwickelbier,None)
Json.obj("name" -> "Plum Zwickelbier").as[Beer]
// JsResultException(errors:List((/barcode,List(ValidationError(error.path.missing,WrappedArray())))))
Json.obj("name" -> "Plum Zwickelbier", "barcode" -> 1234).as[Beer]
// Beer(Plum Zwickelbier,None) --- WTF? Invalid type becomes None!!!
// Reading with readNullable[T]
implicit val beerReads: Reads[Beer] = (
(JsPath \ "name").read[String] and
// Prefer readNullable[T] to read[Option[T]] because the behavior is less weird, see below
(JsPath \ "barcode").readNullable[String]
)(Beer.apply _)
Json.obj("name" -> "Plum Zwickelbier", "barcode" -> JsNull).as[Beer]
// Beer(Plum Zwickelbier,None)
Json.obj("name" -> "Plum Zwickelbier").as[Beer]
// Beer(Plum Zwickelbier,None) --- Key can be missing, nice huh?
Json.obj("name" -> "Plum Zwickelbier", "barcode" -> 123).as[Beer]
// JsResultException(errors:List((/barcode,List(ValidationError(error.expected.jsstring,WrappedArray()))))) --- As expected!
implicit val beerWrites: Writes[Beer] = (
(JsPath \ "name").write[String] and
(JsPath \ "barcode").write[Option[String]]
)(unlift(Beer.unapply))
Json.toJson(Beer("Plum Zwickelbier", None))
// {"name":"Plum Zwickelbier","barcode":null} --- None will always become null
implicit val beerWrites: Writes[Beer] = (
(JsPath \ "name").write[String] and
// Prefer using writeNullable[T] because you probably want to be consistent with readsNullable, see above
(JsPath \ "barcode").writeNullable[String]
)(unlift(Beer.unapply))
Json.toJson(Beer("Plum Zwickelbier", None))
// {"name":"Plum Zwickelbier"} --- None skips the key and the value
/**
* Using Format to define Reads and Writes in one go
*/
// This defines both Reads[Beer] and Writes[Beer] with a compact syntax
// (Only works is reading and writing is symmetrical - which it should be)
implicit val beerFormat: Format[Beer] = (
(JsPath \ "name").format[String] and
(JsPath \ "abv").format[Double]
)(Beer.apply _, unlift(Beer.unapply))
/**
* Error handling
*/
// (json \ "name").as[Double]
// JsResultException
// !!! does not throw, returns None
(json \ "name").as[Option[Double]]
(json \ "doesnotexist").as[Option[String]]
(json \ "doesnotexist").asOpt[String]
(json \ "abv").as[Option[String]]
json.as[Beer]
val invalidBeer = Json.parse("""
{
"name": 123
}
""")
try {
invalidBeer.as[Beer]
// JsResultException(errors:List(
// (/abv, List(ValidationError(error.path.missing, WrappedArray()))),
// (/name, List(ValidationError(error.expected.jsstring, WrappedArray())))
// ))
} catch { case NonFatal(_) => }
/**
* Custom parsers
*/
beerJsObj = Json.parse("""
{
"name": "Doppelgänger Stout",
"abv": 0.048,
"calories": 144,
"expires_at": "2015-12-01T13:20:00Z",
"barcode": null
}
""")
(json \ "expires_at").as[String]
implicit val readsInstant = new Reads[Instant] {
def reads(json: JsValue): JsResult[Instant] =
json.validate[String].flatMap { date: String =>
try JsSuccess(ZonedDateTime.parse(date).toInstant) catch {
case NonFatal(_) => JsError(s"${date} is not a valid date")
}
}
}
(json \ "expires_at").as[Instant]
/**
* Generating Reads/Writes using macros
*/
val macroBeerReads: Reads[Beer] = Json.reads[Beer]
json.as[Beer](macroBeerReads)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment