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 tuples (up to arity-10) as JsArray
- Formats for scala.concurrent.Duration
- Safe formats for
MapviaKeyReadsandKeyWrites - Format builder for empty collections
- UTCFormats for org.joda.time.DateTime
- ScalaCheck generators for JsValue, JsArray, and JsObject
These artifacts were published to Bintray, which was shutdown. These artifacts will NOT be ported to Maven Central.
Pretty much all of these tools become available when you import play.api.libs.json.ops.v4._
- scalacheck-ops: for the ability to convert ScalaCheck
Geninto anIterator
By importing play.api.libs.json.ops.v4._, you get access to:
PlayJsonMacros.nullableReadsmacro that will readnullas[]for all container fields of acase classReads,Format, andOFormatextension methods to recover from exceptions- 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.
ReadsandWritesimplicits for tuple types (encoded as aJsArray)- The
JsValueextension method.asOrThrow[A]which throws a better exception that.as[A] - And handy syntax for the features listed below
Extending the TolerantContainerFormats trait or importing from its companion object will give you the ability to call
.readNullableContainer on a Reads instance. This will allow you to parse null fields as empty collections.
You can also use PlayJsonMacros.nullableReads to create a Reads for a case class that will accept either null
or missing field values for any container fields (Seq, Set, Map, etc) using the same method.
case class Example(values: Seq[Int])
object Example extends TolerantContainerFormats {
val nonMacroExample: Reads[Seq[Int]] = (__ \ "values").readNullableContainer[Seq, Int]
assert(Json.parse("null").as(nonMacroExample) == JsSuccess(Seq()))
assert(Json.parse("[]").as[Example] == JsSuccess(Seq()))
assert(Json.parse("[1]").as[Example] == JsSuccess(Seq(1)))
val macroExample: Reads[Example] = PlayJsonMacros.nullableReads[Example]
assert(Json.parse("{}").as(macroExample) == JsSuccess(Example(Seq())))
assert(Json.parse("""{"values":null}""").as(macroExample) == JsSuccess(Example(Seq())))
assert(Json.parse("""{"values":[]}""").as(macroExample) == JsSuccess(Example(Seq())))
assert(Json.parse("""{"values":[1]}""").as(macroExample) == JsSuccess(Example(Seq(1))))
}You can call .recoverJsError, .recoverTotal, or .recoverWith on a Reads, Format, or OFormat instance.
These methods allow you to recover from exceptions thrown during the reading process into an appropriate JsResult.
object ReadsRecoveryExamples {
// converts all exceptions into a JsError with the exception captured as an argument in the JsonValidationError
val readIntAsString = Reads.of[String].map(_.toInt).recoverJsError
assert(readIntAsString.reads("not a number").isError) // no exception thrown
// converts only the matched exceptions to JsResults, all others continue to throw
val invertReader = Reads.of[String].map(1 / _.toDouble).recoverWith {
case _: ArithmeticException => JsSuccess(Double.MaxValue)
}
invertReader.reads("not a number") // throws NumberFormatException
assert(invertReader.reads("0") == JsSuccess(Double.MaxValue)) // handles ArithmeticException
// converts all exceptions into some value of the right type
val readAbsValueOrSentinel = Reads.of[String].map(_.toInt.abs).recoverTotal(_ => -1)
assert(readAbsValueOrSentinel.reads("not a number") == JsSuccess(-1))
// these can be combined, of course
val safeInvertReader = invertReader.recoverJsError
assert(safeInvertReader.reads("not a number").isError) // no exception thrown
}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.