banno / jsonz   1.5.0

GitHub

Yet another Scala json parsing library blending together the better ideas

Scala versions: 2.11 2.10

Deprecation Notice: jsonz is deprecated and no longer supported. Instead, it is recommended to use circe for json processing.

jsonz

Download

A little of sjson, sjsonapp, scalaz and play's JSON libraries mixed together.

jsonz is a library that implements json serialization and deserialization via typeclasses in scala.

Get jsonz

build.sbt

resolvers += "bintray-banno-oss-releases" at "http://dl.bintray.com/banno/oss"

libraryDependencies ++= Seq(
  "jsonz" %% "jsonz" % "1.5.0"
)

Differences between other json parsing libraries

Default Values

If a model (typically case classes) has default values, they may be omitted from json when reading that json.

import jsonz._
import jsonz.DefaultFormats._
object DefaultValues {
  case class Foo(thing: Int, other: Option[String] = None)
  implicit val format = productFormat2("thing", "other")(Foo.apply)(Foo.unapply)
  val json = """{"thing": 1}"""
  Jsonz.fromJsonStr[Foo](json)
}

Failures

Jsonz is different about failures. When parsing json, failures are accumulated and delivered at the end. The type JsonzValidation[T] is a type alias for scalaz's Validation[NonEmptyList[JsFailure], T] so the user will receive either a scalaz.Success with an instance of the model filled in with the data from the json, or a scalaz.Failure that contains failures that occured during parsing. These failures could be from missing fields, or malformly typed elements.

No Exceptions

Jsonz tries really hard to never throw exceptions. Other json libraries we encountered would throw exceptions at malformed or key-missing json. Instead jsonz will return a parsable failure for the user to parse.

Usage

Reading and Writing from models (case classes)

Jsonz provides ProductFormats for use over anything that abstracts over scala.Product. This means your typical case class can be written to json and read back out.

import jsonz._
import jsonz.DefaultFormats._

object Example {
    case class Foo(thing: Int, other: String)
    implicit val fooFormat = productFormat2("thing", "other")(Foo.apply)(Foo.unapply)

    Jsonz.fromJsonStr[Foo]("""{"thing":12, "other":"hi"}""") // JsonzValidation[Foo]
    Jsonz.toJsonStr(Foo(12, "hi"))
}

Out of the box type support

jsonz provides support for reading and writing the following types out of the box: String, Short, Int, Long, Float, Double, BigDecimal, Boolean, JsValue. The following types require a Format for the generic types to be in scope: Option[T], Map[String, V], Traversable[T], Array[T], scalaz.NonEmptyList[T], jsonz.JsFailure, Either[L, R], and scalaz.Validation[E, A].

Enumerations

Java's java.lang.Enum and Scala's scala.Enumeration are supported out of the box, simply mix in (or import the companion object) jsonz.EnumerationFormats into the scope where you wish to define the implicit format and call javaEnumerationFormat or scalaEnumerationFormat with the enumeration instance passed in.

// Defined in Day.java
public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
import jsonz._
import jsonz.DefaultFormats._
object Animals extends Enumeration {
    val Tiger = Value
    val Lion = Value
    val Swallow = Value
}
implicit val AnimalsFormat: Format[Animals.Value] = scalaEnumerationFormat(Animals)
implicit val DaysFormat: Format[Day] = javaEnumerationFormat[Day](Day.MONDAY)

Recursive or Composed Values

Given a recursive type you will need to wrap the format in the lazyFormat method.

import jsonz._
import jsonz.DefaultFormats._
case class RecursiveType(rs: List[RecursiveType])
implicit lazy val RecursiveTypeFormat: Format[RecursiveType] = lazyFormat(productFormat1("rs")(RecursiveType.apply)(RecursiveType.unapply))

Similarly with compositions, such as a case class having another case class as a member:

import jsonz._
import jsonz.DefaultFormats._
case class OuterType(child: InnerType)
case class InnerType(message: String)
implicit lazy val OuterTypeFormat: Format[OuterType] = lazyFormat(productFormat1("child")(OuterType.apply)(OuterType.unapply))
implicit lazy val InnerTypeFormat: Format[InnerType] = productFormat1("message")(InnerType.apply)(InnerType.unapply)

Note: Flipping the order of the above definitions would eliminate the need for lazyFormat, however if one format is in a 3rd party package lazyFormat would allow you to define this structure.

Joda-Time

Joda-Time is typically the first library chosen for performing date/time operations on the JVM. Jsonz offers support for reading and writing it's org.joda.time.DateTime class via the following imports. The formats used to try and parse are: ISO 8601 (with and without millis), yyyy-MM-dd, and RFC 1123 (EEE, dd MMM yyyy HH:mm:ss 'GMT').

Note: Check the build.sbt for the specific version jsonz relies on.

import jsonz._
import jsonz.joda.JodaTimeFormats._

Spray

Spray versions 1.1.x, 1.2.x, and 1.3.x are supported to read entities and complete requests as json via jsonz. To use the following import will need to be used, or to mixin the associated trait under the same name.

Note: Check the build.sbt for the specific version jsonz relies on.

import jsonz.spray.JsonzMarshalling._ // companion object
import jsonz.spray.JsonzMarshalling   // trait

Specs2

Specs2 matchers are provided by jsonz. In order to use them simply mix in (or import via the companion object) jsonz.specs2.Specs2JsonzTestkit. Then the following matchers are visible. For examples refer to the test over these matchers.

Note: Check the build.sbt for the specific version jsonz relies on.

def haveJsonField[T](field: Symbol, expected: T)(implicit tr: Reads[T], m: Manifest[T]): Matcher[JsValue]
def haveJsonField(field: Symbol): Matcher[JsValue]
def haveNestedJsonField[T <: JsValue](fields: Symbol*)(expected: T): Matcher[JsValue]
def haveNullJsonField(field: Symbol): Matcher[JsValue]
def haveNullNestedJsonField(fields: Symbol*): Matcher[JsValue]
def haveJsonFieldOfSize(field: Symbol, expectedSize: Int): Matcher[JsValue]
def haveJsonFieldWhichContains[T <: JsValue](field: Symbol, containsField: Symbol, expected: T): Matcher[JsValue]
def beSuccessWhich[T](f: (T => Boolean)): Matcher[Validation[_,T]]
def containFailure[A, B](a: A): Matcher[ValidationNel[A, B]]
def haveFailureCount[A, B](n: Int): Matcher[ValidationNel[A, B]]

Custom Types

You can use the methods defined in jsonz.Fields to extract out parts of a json object at a much more fine-grained detail. An exmaple:

package api
import scalaz.Success
import jsonz._
import jsonz.Fields._
import jsonz.JsFailure._

case class UserUpdateParams(firstName: Option[String] = None, lastName: Option[String] = None)

object Formats extends DefaultFormats {
  case class NonEmptyString(str: String)
  def nonEmptyStringToString(nes: NonEmptyString): String = nes.str

  implicit val UserUpdateParamsReads: Reads[UserUpdateParams] = new Reads[UserUpdateParams] {
    def reads(js: JsValue): JsonzValidation[UserUpdateParams] = {
      def str(key: String): JsonzValidation[Option[String]] = field[Option[String]](key, js).flatMap {
        case s@Some(str) => if (str.trim.nonEmpty) Success(s) else jsFailureValidationNel("is empty")
        case other => Success(other)
      }

      for {
        firstName <- str("firstName")
        lastName <- str("lastName")
      } yield UserUpdateParams(firstName, lastName)
    }
  }
}

Scalaz Instances

Mixing in the jsonz.JsValueInstances (which extends from jsonz.JsValueEquality) will provide scalaz.Monoid and scalaz.Equal instances for JsValue's sub types, but not JsValue itself. These are often useful if you want to combine two like subtypes of JsValue.

Contributing

Create a PR against master and we'll take a looksie at it. If it passes CI builds it'll probably be merged.

License

This is released under the Apache License, Version 2.0. See LICENSE