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
Map
viaKeyReads
andKeyWrites
- 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
Gen
into anIterator
By importing play.api.libs.json.ops.v4._
, you get access to:
PlayJsonMacros.nullableReads
macro that will readnull
as[]
for all container fields of acase class
Reads
,Format
, andOFormat
extension methods to recover from exceptions- Many extension methods for the
play.api.libs.json.Json
Format.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.pure
for reading and writing a constant valueFormat.empty
for reading or writing an empty collection- In Play 2.3, the
Json.format
andJson.writes
macros would returnFormat
andWrites
instead ofOFormat
andOWrites
, even though the macros would only produce these types. The play-json-ops for Play 2.3 provides aJson.oformat
andJson.owrites
which uses the underlying Play Json macros, but it casts the results.
Reads
andWrites
implicits for tuple types (encoded as aJsArray
)- The
JsValue
extension 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.formatWithType
and theJson.format
macro.This will append the key field (even if it isn't in the case class constructor args) to the output json.
-
Create an implicit
TypeKeyExtractor
for the generic trait or abstract class on the companion object of that class.This is required for the
Json.formatWithType
to 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
Format
for your generic trait or abstract class usingJson.formatAbstract
by 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
JsValue
s or JsObject
s. 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.