Augments the Play Json library with some helpful implicits and tools for:
- Creating formats for traits and abstract classes
- Safely printing error messages with redacted sensitive data using implicit transformations
- Formats for all tuples as JsArray
- ScalaCheck generators for JsValue, JsArray, and JsObject
- Formats for scala.concurrent.Duration
- UTCFormats for org.joda.time.DateTime
- Compile-time Json.oformat and Json.owrites macros (Play 2.3 only)
Pretty much all of these tools become available when you import play.api.libs.json.ops._
- scalacheck-ops: for the ability to convert ScalaCheck
Geninto anIterator
By importing play.api.libs.json.ops._, you get access to implicits that provide:
- Many extension methods for the
play.api.libs.json.JsonFormat.of[A],OFormat.of[A], andOWrites.of[A]for summoning formats the same asReads.of[A]andWrites.of[A]Format.asEither[A, B]for reading and writing an either value based on some conditionFormat.asString[A]for reading and writing a wrapper type as a stringFormat.purefor reading and writing a constant valueFormat.emptyfor reading or writing an empty collection- In Play 2.3, the
Json.formatandJson.writesmacros would returnFormatandWritesinstead ofOFormatandOWrites, even though the macros would only produce these types. The play-json-ops for Play 2.3 provides aJson.oformatandJson.owriteswhich uses the underlying Play Json macros, but it casts the results.
ReadsandWritesfor tuple types by writing the result as aJsArray- The
JsValueextension method.asOrThrow[A]which throws a better exception that.as[A] - And handy syntax for the features listed below
To get free test coverage, just extend PlayJsonFormatSpec[T] where T is a serializable type that you
would like to create a suite of tests for. All it requires is a ScalaCheck generator of the same type or
a sequence of examples.
This will use ScalaTest to create the test cases, however it will work just as well with Specs2
case class Example(value: String)
object Example {
implicit val format = Json.format[Example]
}
object ExampleGenerators {
implicit def arbExample(implicit arbString: Arbitrary[String]): Arbitrary[Example] =
Arbitrary(arbString.map(Example(_)))
}
import ExampleGenerators._
// Free unit tests for serializing and deserializing Example values
// Also works with implicit Shrink[Example]
class ExampleFormatSpec extends PlayJsonFormatSpec[Example]
The following example shows how you can create a Format for the Generic trait using Json.formatAbstract.
This method requires an implicit TypeKeyExtractor[Generic], which is used to pull a "key" value from some
field in the json / model. This key value is then matched on by a provided partial function from key to
format: Any => OFormat[_ <: Generic].
The pattern works as follows:
-
Create the formats of each of the specific formats using
Json.formatWithTypeand theJson.formatmacro.This will append the key field (even if it isn't in the case class constructor args) to the output json.
-
Create an implicit
TypeKeyExtractorfor the generic trait or abstract class on the companion object of that class.This is required for the
Json.formatWithTypeto work properly and avoids repeating unnecessary boilerplate on each of the specific serializers to write out the key or the generic serializer to read the key. -
Finally, define an implicit
Formatfor your generic trait or abstract class usingJson.formatAbstractby providing a partial function from the extracted key (from #2) to the specific serializer (from #1). Any unmatched keys will throw an exception.
import play.api.libs.json._
import play.api.libs.json.ops._
sealed trait Generic {
def key: String
}
object Generic {
implicit val extractor: TypeKeyExtractor[Generic] =
Json.extractTypeKey[Generic].usingKeyField(_.key, __ \ "kind")
implicit val format: OFormat[Generic] = Json.formatAbstract[Generic] {
case SpecificA.key => OFormat.of[SpecificA]
case SpecificB.key => OFormat.of[SpecificB]
}
}
case class SpecificA(value: String) extends Generic {
override def key: String = SpecificA.key
}
object SpecificA {
final val key = "A"
// NOTE: You will need to use Json.oformat for Play 2.3.x
implicit val format: OFormat[SpecificA] = Json.formatWithTypeKeyOf[Generic].addedTo(Json.format[SpecificA])
}
case class SpecificB(value: String) extends Generic {
override def key: String = SpecificB.key
}
object SpecificB {
final val key = "B"
implicit val format: OFormat[SpecificB] = Json.formatWithTypeKeyOf[Generic].addedTo(Json.format[SpecificB])
}
case object SpecificC extends Generic {
final val key = "C"
implicit val format: OFormat[this.type] = OFormat.pure(this, Generic.extractor.writeKeyToJson(this))
}You can add implicit Json serializers by importing DurationFormat.string or DurationFormat.array depending
on the format you want.
You can also extend ArrayDurationFormat or StringDurationFormat for the same effect, but it requires that
you also extend an ImplicitDurationReads. A good default is to extend ForgivingDurationReads as this will
read either format.
Ok, now how the formats look in Json:
-
ArrayDurationFormat[1, "seconds"]
-
StringDurationFormat"1 second"
ScalaCheck is a very simple and powerful library for property-based testing.
Fun fact: It is the only library dependency of the Scala compiler
Ok, so assuming you are already familiar with ScalaCheck now... Let's say you want to generate arbitrary
JsValues or JsObjects. All you have to do is extend JsValueGenerators in your test class and voila!
By default the maximum depth of the JsValue trees is set to JsValueGenerators.maxDepth and the maximum
number of fields for JsObject and values for JsArray is set to JsValueGenerators.maxWidth. You can
override this in local scope by providing an implicit Depth or Width type value:
implicit val maxDepth: Depth = 4
forAll() { (json: JsValue) =>
// ...
}or passing the values explicitly:
forAll(genJsValue(maxDepth = 4, maxWidth = 12)) { (json: JsValue) =>
// ...
}Note: I encountered a compiler bug when overriding implicits in a local scope where the compiler would
NOT throw the normal "ambiguous implicit values" exception and instead use the depth defined in the outer
scope. Just be sure not to define ambiguous implicit Depth and Width values and everything works great.