All Downloads are FREE. Search and download functionalities are using the official Maven repository.

spray.json.FamilyFormats.scala Maven / Gradle / Ivy

The newest version!
// Copyright: 2010 - 2018 https://github.com/ensime/ensime-server/graphs
// License: http://www.gnu.org/licenses/lgpl-3.0.en.html
package spray.json

import scala.collection.immutable.ListMap

import shapeless._, labelled.{ field, FieldType }

/**
 * Automatically create product/coproduct marshallers (i.e. families
 * of sealed traits and case classes/objects) for spray-json.
 *
 * Shapeless allows us to view sealed traits as "co-products" (aka
 * `Coproduct`s) and to view case classes / objects as "products" (aka
 * `HList`s).
 *
 * Here we write marshallers for `HList`s and `Coproduct`s and a converter
 * to/from the generic form.
 *
 * =Customisation=
 *
 * Users may provide an implicit `CoproductHint[T]` for their sealed
 * traits, allowing the disambiguation scheme to be customised.
 * Some variants are provided to cater for common needs.
 *
 * Users may also provide an implicit `ProductHint[T]` for their case
 * classes, allowing wire format field names to be customised and to
 * specialise the handling of `JsNull` entries. By default, `None`
 * optional parameters are omitted and any formatter that outputs
 * `JsNull` will be respected.
 *
 * =Performance=
 *
 * TL;DR these things are bloody fast, don't even think about runtime
 * performance concerns unless you have hard proof that these
 * serialisers are a bottleneck.
 *
 * However, **compilation times** may be negatively impacted. Many of
 * the problems are fundamental performance issues in the Scala
 * compiler. In particular, compile times appear to be quadratic with
 * respect to the number of case classes/objects in a sealed family,
 * with compile times becoming unbearable for 30+ implementations of a
 * sealed trait. It may be wise to use encapsulation to reduce the
 * number of implementations of a sealed trait if this becomes a
 * problem. For more information see
 * https://github.com/milessabin/shapeless/issues/381
 *
 * Benchmarking has shown that these formats are within 5% of the
 * performance of hand-crafted spray-json family format serialisers.
 * The extra overhead can be explained by the conversion to/from
 * `LabelledGeneric`, and administration around custom type hinting.
 * These are all just churn of small objects - not computational - and
 * is nothing compared to the performance bottleneck introduced by
 * using an `Either` (which uses exception throwing for control flow),
 * e.g. https://github.com/spray/spray-json/issues/133 or how `Option`
 * fields used to be handled
 * https://github.com/spray/spray-json/pull/136
 *
 * The best performance is obtained by constructing an "explicit
 * implicit" (see the tests for examples) for anything that you
 * specifically wish to convert to / from (e.g. the top level family,
 * if it is on a popular endpoint) because it will reuse the
 * formatters at all stages in the hierarchy. However, the overhead
 * for not defining the `implicit val` is less that you might expect
 * (it only accounts for another 5%) whereas eagerly defining
 * `implicit val`s for everything in the hierarchy can
 * (counterintuitively) slow things down by 50%.
 *
 * Logging at the `trace` level is enabled to allow visibility of when
 * formatters are being instantiated.
 *
 * =Caveats=
 *
 * If shapeless fails to derive a family format for you, it won't tell
 * you what was missing in your tree. e.g. if you have a `UUID` in a
 * deeply nested case class in a `MyFamily` sealed trait, it will
 * simply say "can't find implicit for MyFamily" not "can't find
 * implicit for UUID". When that happens, you just have to work out
 * what is missing by trial and error. Sorry!
 *
 * Also, the Scala compiler has some funny [order dependency
 * rules](https://issues.scala-lang.org/browse/SI-7755) which will
 * sometimes make it look like you're missing an implicit for an
 * element. The best way to avoid this is:
 *
 * 1. define the protocol/formatters in sibling, or otherwise
 *    independent, non-cyclic, packages. In particular, if you define
 *    your domain objects in `foo.domain` and your formats in
 *    `foo.formats`, note that you will not be able to access the
 *    formats from the `foo` parent package (this catches a lot of
 *    people out). Another approach is to use separate projects for
 *    the domain and formats, which avoids the problem entirely whilst
 *    allowing you to provide zero dependency packages of your domain
 *    objects to downstream consumers (I believe this to be good
 *    practice in a microservices world, as it effectively means
 *    exporting your schema).
 *
 * 2. define all your custom rules in an `object` that extends
 *    `FamilyFormats` so that the implicit resolution priority rules
 *    work in your favour (see tests for an example of this style).
 *    The derived `familyFormat` will win over implicit formats that
 *    have been inherited from a lower implicit scope, so you will
 *    often have to explicitly bring them back into the higher scope
 *    by listing each -- see FamilyFormats for an example using
 *    `SymbolFormat` and a user-defined format. i.e. provide an
 *    explicit `implicit val symbolFormat = SymbolJsonFormat`,
 *    similarly for `JsObjectFormat`.
 */
trait FamilyFormats extends MiddlePriorityFamilyFormats {
  this: BasicFormats with StandardFormats =>

  // scala compiler doesn't like spray-json's use of a type alias in the sig
  override implicit def optionFormat[T: JsonFormat]: JsonFormat[Option[T]] =
    new OptionFormat[T]
}
object FamilyFormats extends DefaultJsonProtocol with FamilyFormats

private[json] trait MiddlePriorityFamilyFormats
    extends LowPriorityFamilyFormats {
  this: StandardFormats with FamilyFormats =>

  /**
   * Format for `LabelledGenerics` that uses the `HList` marshaller below.
   *
   * `Blah.Aux[T, Repr]` is a trick to work around scala compiler
   * constraints. We'd really like to have only one type parameter
   * (`T`) implicit list `g: LabelledGeneric[T], f:
   * Cached[Strict[JsonFormat[T.Repr]]]` but that's not possible.
   */
  implicit def familyFormatWithDefault[T, Repr, DefaultRepr <: HList](
    implicit
    gen: LabelledGeneric.Aux[T, Repr],
    default: Default.AsOptions.Aux[T, DefaultRepr],
    sg: Cached[Strict[WrappedRootJsonFormatWithDefault[T, Repr, DefaultRepr]]]
  ): RootJsonFormat[T] = new RootJsonFormat[T] {
    def read(j: JsValue): T   = gen.from(sg.value.value.read(j, default()))
    def write(t: T): JsObject = sg.value.value.write(gen.to(t))
  }
}

/* low priority implicit scope so user-defined implicits take precedence */
private[json] trait LowPriorityFamilyFormats extends JsonFormatHints {
  this: StandardFormats with MiddlePriorityFamilyFormats =>

  /**
   * a `JsonFormat[HList]` or `JsonFormat[Coproduct]` would not retain the
   * type information for the full generic that it is serialising.
   * This allows us to pass the wrapped type, achieving: 1) custom
   * `CoproductHint`s on a per-trait level 2) configurable `null` behaviour
   * on a per product level 3) clearer error messages.
   *
   * This is intentionally not part of the `JsonFormat` hierarchy to
   * avoid ambiguous implicit errors.
   */
  abstract class WrappedRootJsonFormat[Wrapped, SubRepr](
    implicit
    tpe: Typeable[Wrapped]
  ) {
    final def read(j: JsValue): SubRepr = j match {
      case jso: JsObject => readJsObject(jso)
      case other         => unexpectedJson[Wrapped](other)
    }
    def readJsObject(j: JsObject): SubRepr
    def write(v: SubRepr): JsObject
  }

  /**
   * Subclass of the the `WrappedRootJsonFormat` that provide a way to
   * deserialize product with a default value.
   */
  abstract class WrappedRootJsonFormatWithDefault[Wrapped, SubRepr, DefaultRepr](
    implicit
    tpe: Typeable[Wrapped]
  ) extends WrappedRootJsonFormat[Wrapped, SubRepr] {
    final def read(j: JsValue, default: DefaultRepr): SubRepr = j match {
      case jso: JsObject => readJsObjectWithDefault(jso, default)
      case other         => unexpectedJson[Wrapped](other)
    }
    def readJsObjectWithDefault(j: JsObject, default: DefaultRepr): SubRepr
    def readJsObject(j: JsObject): SubRepr =
      deserError(
        s"read should never be from WrappedRootJsonFormatWithDefault, $j"
      )
  }

  // save an object alloc every time and gives ordering guarantees
  private[this] val emptyJsObject = new JsObject(ListMap())

  // HNil is the empty HList
  implicit def hNilFormat[Wrapped](
    implicit
    t: Typeable[Wrapped]
  ): WrappedRootJsonFormatWithDefault[Wrapped, HNil, HNil] =
    new WrappedRootJsonFormatWithDefault[Wrapped, HNil, HNil] {
      def readJsObjectWithDefault(j: JsObject, default: HNil) =
        HNil // usually a populated JsObject, contents irrelevant
      def write(n: HNil) = emptyJsObject
    }

  // HList with a FieldType at the head
  implicit def hListFormat[Wrapped,
                           Key <: Symbol,
                           Value,
                           Remaining <: HList,
                           D <: HList](
    implicit
    t: Typeable[Wrapped],
    ph: ProductHint[Wrapped],
    key: Witness.Aux[Key],
    jfh: Lazy[JsonFormat[Value]], // svc doesn't need to be a RootJsonFormat
    jft: WrappedRootJsonFormatWithDefault[Wrapped, Remaining, D]
  ): WrappedRootJsonFormatWithDefault[Wrapped,
                                      FieldType[Key, Value] :: Remaining,
                                      Option[Value] :: D] =
    new WrappedRootJsonFormatWithDefault[Wrapped,
                                         FieldType[Key, Value] :: Remaining,
                                         Option[Value] :: D] {
      private[this] val fieldName = ph.fieldName(key.value)

      private[this] def missingFieldError(j: JsObject): Nothing =
        deserError(s"missing $fieldName, found ${j.fields.keys.mkString(",")}")

      def readJsObjectWithDefault(j: JsObject, default: Option[Value] :: D) = {
        val resolved: Value = (j.fields.get(fieldName), jfh.value) match {
          // (None, _) means the value is missing in the wire format
          case (None, f) if ph.nulls == NeverJsNull =>
            f.read(JsNull)

          case (None, f) if ph.nulls == UseDefaultJsNull =>
            default.head.getOrElse(f.read(JsNull))

          case (None, f) if ph.nulls == AlwaysJsNull =>
            missingFieldError(j)

          case (None, f: OptionFormat[_])
              if ph.nulls == JsNullNotNone || ph.nulls == AlwaysJsNullTolerateAbsent =>
            None.asInstanceOf[Value]

          case (Some(JsNull), f: OptionFormat[_])
              if ph.nulls == JsNullNotNone =>
            f.readSome(JsNull)

          case (Some(value), f) =>
            f.read(value)

          case _ =>
            missingFieldError(j)
        }
        val remaining = jft.read(j, default.tail)
        field[Key](resolved) :: remaining
      }

      def write(ft: FieldType[Key, Value] :: Remaining) =
        (jfh.value.write(ft.head), jfh.value) match {
          // (JsNull, _) means the underlying formatter serialises to JsNull
          case (JsNull, _) if ph.nulls == NeverJsNull =>
            jft.write(ft.tail)
          case (JsNull, _) if ph.nulls == JsNullNotNone & ft.head == None =>
            jft.write(ft.tail)
          case (value, _) =>
            jft.write(ft.tail) match {
              case JsObject(others) =>
                // when gathering results, we must remember that 'other'
                // is to the right of us and this seems to be the
                // easiest way to prepend to a ListMap
                JsObject(ListMap(fieldName -> value) ++: others)
              case other =>
                serError(s"expected JsObject, seen $other")
            }
        }
    }

  // CNil is the empty co-product. It's never called because it would
  // mean a non-existant sealed trait in our interpretation.
  implicit def cNilFormat[Wrapped](
    implicit
    t: Typeable[Wrapped]
  ): WrappedRootJsonFormat[Wrapped, CNil] =
    new WrappedRootJsonFormat[Wrapped, CNil] {
      def readJsObject(j: JsObject) =
        deserError(s"read should never be called for CNil, $j")
      def write(c: CNil) = serError("write should never be called for CNil")
    }

  // Coproduct with a FieldType at the head
  implicit def coproductFormat[Wrapped,
                               Name <: Symbol,
                               Instance,
                               Remaining <: Coproduct](
    implicit
    tpe: Typeable[Wrapped],
    th: CoproductHint[Wrapped],
    key: Witness.Aux[Name],
    jfh: Lazy[RootJsonFormat[Instance]],
    jft: WrappedRootJsonFormat[Wrapped, Remaining]
  ): WrappedRootJsonFormat[Wrapped, FieldType[Name, Instance] :+: Remaining] =
    new WrappedRootJsonFormat[Wrapped, FieldType[Name, Instance] :+: Remaining] {
      def readJsObject(j: JsObject) = th.read(j, key.value) match {
        case Some(product) =>
          val recovered = jfh.value.read(product)
          Inl(field[Name](recovered))

        case None =>
          Inr(jft.read(j))
      }

      def write(lr: FieldType[Name, Instance] :+: Remaining) = lr match {
        case Inl(l) =>
          jfh.value.write(l) match {
            case j: JsObject => th.write(j, key.value)
            case other       => serError(s"expected JsObject, got $other")
          }

        case Inr(r) =>
          jft.write(r)
      }
    }

  /**
   * Format for `LabelledGenerics` that uses the `Coproduct`
   * marshaller above.
   *
   * `Blah.Aux[T, Repr]` is a trick to work around scala compiler
   * constraints. We'd really like to have only one type parameter
   * (`T`) implicit list `g: LabelledGeneric[T], f:
   * Cached[Strict[JsonFormat[T.Repr]]]` but that's not possible.
   */
  implicit def familyFormat[T, Repr](
    implicit
    gen: LabelledGeneric.Aux[T, Repr],
    sg: Cached[Strict[WrappedRootJsonFormat[T, Repr]]]
  ): RootJsonFormat[T] = new RootJsonFormat[T] {
    def read(j: JsValue): T   = gen.from(sg.value.value.read(j))
    def write(t: T): JsObject = sg.value.value.write(gen.to(t))
  }
}

trait JsonFormatHints {
  trait CoproductHint[T] {

    /**
     * Given the `JsObject` for the sealed family, disambiguate and
     * extract the `JsObject` associated to the `Name` implementation
     * (if available) or otherwise return `None`.
     */
    def read[Name <: Symbol](j: JsObject, n: Name): Option[JsObject]

    /**
     * Given the `JsObject` for the contained product type of `Name`,
     * encode disambiguation information for later retrieval.
     */
    def write[Name <: Symbol](j: JsObject, n: Name): JsObject

    /**
     * Override to provide custom field naming.
     * Caching is recommended for performance.
     */
    protected def fieldName(orig: String): String = orig
  }

  /**
   * Product types are disambiguated by a `{"key":"value",...}`. Of
   * course, this will fail if the product type has a field with the
   * same name as the key. The default key is the word "type" which
   * is a keyword in Scala so unlikely to collide with too many case
   * classes.
   *
   * This variant is most common in JSON serialisation schemes and
   * well supported by other frameworks.
   */
  class FlatCoproductHint[T: Typeable](key: String) extends CoproductHint[T] {
    def read[Name <: Symbol](j: JsObject, n: Name): Option[JsObject] =
      j.fields.get(key) match {
        case Some(JsString(hint)) if hint == fieldName(n.name) => Some(j)
        case Some(JsString(hint))                              => None
        case _ =>
          deserError(s"missing $key, found ${j.fields.keys.mkString(",")}")
      }

    // puts the typehint at the head of the field list
    def write[Name <: Symbol](j: JsObject, n: Name): JsObject = {
      // runtime error, would be nice if we could check this at compile time
      if (j.fields.contains(key))
        serError(
          s"typehint '$key' collides with existing field ${j.fields(key)}"
        )
      JsObject(ListMap(key -> JsString(fieldName(n.name))) ++: j.fields)
    }
  }

  /**
   * Product types are disambiguated by an extra JSON map layer
   * containing a single key which is the name of the type of product
   * contained in the value. e.g. `{"MyType":{...}}`
   *
   * This variant may be more appropriate for non-polymorphic schemas
   * such as MongoDB and Mongoose (consider using the above format on
   * your endpoints, and this format when persisting).
   */
  class NestedCoproductHint[T: Typeable] extends CoproductHint[T] {
    def read[Name <: Symbol](j: JsObject, n: Name): Option[JsObject] =
      j.fields.get(fieldName(n.name)).map {
        case jso: JsObject => jso
        case other         => unexpectedJson(other)
      }

    def write[Name <: Symbol](j: JsObject, n: Name): JsObject =
      JsObject(fieldName(n.name) -> j)
  }

  implicit def coproductHint[T: Typeable]: CoproductHint[T] =
    new FlatCoproductHint[T]("type")

  /**
   * Sometimes the wire format needs to match an existing format and
   * `JsNull` behaviour needs to be customised. This allows null
   * behaviour to be defined at the product level. Field level control
   * is only possible with a user-defined `RootJsonFormat`.
   */
  sealed trait JsNullBehaviour

  /** All values serialising to `JsNull` will be included in the wire format. Ambiguous. */
  case object AlwaysJsNull extends JsNullBehaviour

  /** Option values of `None` are omitted, but `Some` values of `JsNull` are retained. Default. */
  case object JsNullNotNone extends JsNullBehaviour

  /** No values serialising to `JsNull` will be included in the wire format. Ambiguous. */
  case object NeverJsNull extends JsNullBehaviour

  /** Same as AlwaysJsNull when serialising, with missing values treated as optional upon deserialisation. Ambiguous. */
  case object AlwaysJsNullTolerateAbsent extends JsNullBehaviour

  /** Use the case class default value provided for the field when available. Ambiguous. */
  case object UseDefaultJsNull extends JsNullBehaviour

  trait ProductHint[T] {
    def nulls: JsNullBehaviour                     = JsNullNotNone
    def fieldName[Key <: Symbol](key: Key): String = key.name
  }
  implicit def productHint[T: Typeable] = new ProductHint[T] {}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy