Last active
July 9, 2017 16:58
-
-
Save salomvary/2d919da05b23010570e7 to your computer and use it in GitHub Desktop.
PlayJson Crash Course
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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