spray-json derived codecs

Build Status Latest version codecov.io Scala Steward badge License

JsonFormat derivation for algebraic data types, inspired by Play Json Derived Codecs.

The derivation built with Scala 2.x is powered by shapeless, whereas the one built with Scala 3 is based on the new Type Class Derivation language API.

The derivation currently supports:

  • sum types
  • product types
  • recursive types
  • polymorphic types

This library is built with Sbt 1.5.2 or later, and its master branch is built with Scala 2.13.6 by default but also cross-builds for 3 and 2.12.


NOTE

Scala 2.11 is no longer supported. The latest version available for scala 2.11 is 2.2.2.


Installation

If you use sbt add the following dependency to your build file:

libraryDependencies += "io.github.paoloboni" %% "spray-json-derived-codecs" % "<version>"

Usage

For automatic derivation, add the following import:

import spray.json.derived.auto._

If you prefer to explicitly define your formats, then you can use semi-auto derivation:

import spray.json.derived.semiauto._

Examples

Product types

Auto derivation
import spray.json._
import spray.json.derived.auto._
import spray.json.DefaultJsonProtocol._

case class Cat(name: String, livesLeft: Int)

object Test extends App {
  val oliver: Cat = Cat("Oliver", 7)
  val encoded     = oliver.toJson
  
  assert(encoded == """{"livesLeft":7,"name":"Oliver"}""".parseJson)
  assert(encoded.convertTo[Cat] == oliver)
}
Semi-auto derivation
import spray.json._
import spray.json.derived.semiauto._
import spray.json.DefaultJsonProtocol._

case class Cat(name: String, livesLeft: Int)

object Test extends App {
  implicit val format: JsonFormat[Cat] = deriveFormat[Cat]

  val oliver: Cat = Cat("Oliver", 7)
  val encoded     = oliver.toJson

  assert(encoded == """{"livesLeft":7,"name":"Oliver"}""".parseJson)
  assert(encoded.convertTo[Cat] == oliver)
}

Union types

Union types are encoded by using a discriminator field, which by default is type.

import spray.json._
import spray.json.derived.auto._
import spray.json.DefaultJsonProtocol._

sealed trait Pet
case class Cat(name: String, livesLeft: Int)   extends Pet
case class Dog(name: String, bonesHidden: Int) extends Pet

object Test extends App {
  val oliver: Pet   = Cat("Oliver", 7)
  val encodedOliver = oliver.toJson
  assert(encodedOliver == """{"livesLeft":7,"name":"Oliver","type":"Cat"}""".parseJson)
  assert(encodedOliver.convertTo[Pet] == oliver)

  val albert: Pet   = Dog("Albert", 3)
  val encodedAlbert = albert.toJson
  assert(encodedAlbert == """{"bonesHidden":3,"name":"Albert","type":"Dog"}""".parseJson)
  assert(encodedAlbert.convertTo[Pet] == albert)
}

The discriminator can be customised by annotating the union type with the @Discriminator annotation:

import spray.json._
import spray.json.derived.auto._
import spray.json.DefaultJsonProtocol._
import spray.json.derived.Discriminator

@Discriminator("petType")
sealed trait Pet
case class Cat(name: String, livesLeft: Int)   extends Pet
case class Dog(name: String, bonesHidden: Int) extends Pet

object Test extends App {
  val oliver: Pet   = Cat("Oliver", 7)
  val encodedOliver = oliver.toJson
  assert(encodedOliver == """{"livesLeft":7,"name":"Oliver","petType":"Cat"}""".parseJson)
  assert(encodedOliver.convertTo[Pet] == oliver)
}

Recursive types

import spray.json._
import spray.json.derived.auto._
import spray.json.DefaultJsonProtocol._

sealed trait Tree
case class Leaf(s: String)            extends Tree
case class Node(lhs: Tree, rhs: Tree) extends Tree

object Test extends App {

  val obj: Tree = Node(Node(Leaf("1"), Leaf("2")), Leaf("3"))
  val encoded   = obj.toJson
  val expectedJson =
    """{
      |  "lhs": {
      |    "lhs": {
      |      "s": "1",
      |      "type": "Leaf"
      |    },
      |    "rhs": {
      |      "s": "2",
      |      "type": "Leaf"
      |    },
      |    "type": "Node"
      |  },
      |  "rhs": {
      |    "s": "3",
      |    "type": "Leaf"
      |  },
      |  "type": "Node"
      |}
      |""".stripMargin
  assert(encoded == expectedJson.parseJson)
  assert(encoded.convertTo[Tree] == obj)
}

Polymorphic types

import spray.json._
import spray.json.derived.auto._
import spray.json.DefaultJsonProtocol._

case class Container[T](value: T)

object Test extends App {

  val cString: Container[String] = Container("abc")
  val cStringEncoded             = cString.toJson
  assert(cStringEncoded == """{"value":"abc"}""".parseJson)
  assert(cStringEncoded.convertTo[Container[String]] == cString)

  val cInt: Container[Int] = Container(123)
  val cIntEncoded          = cInt.toJson
  assert(cIntEncoded == """{"value":123}""".parseJson)
  assert(cIntEncoded.convertTo[Container[Int]] == cInt)
}

Undefined optional members

By default, undefined optional members are not rendered:

import spray.json._
import spray.json.derived.auto._
import spray.json.DefaultJsonProtocol._

case class Dog(toy: Option[String])

object Test extends App {
  val aDog        = Dog(toy = None)
  val aDogEncoded = aDog.toJson
  assert(aDogEncoded.compactPrint == "{}")
}

It's possible to render undefined optional members as null values by specifying an alternative configuration. Just specify the alternative configuration as implicit value and enable the renderNullOptions flag:

import spray.json._
import spray.json.derived.Configuration
import spray.json.derived.auto._
import spray.json.DefaultJsonProtocol._

case class Dog(toy: Option[String])

object Test extends App {

  implicit val conf: Configuration = Configuration(renderNullOptions = true)

  val aDog        = Dog(toy = None)
  val aDogEncoded = aDog.toJson
  assert(aDogEncoded.compactPrint == """{"toy":null}""")
}

License

spray-json-derived-codecs is licensed under APL 2.0.