trait FamilyFormats extends LowPriorityFamilyFormats

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 Coproducts) and to view case classes / objects as "products" (aka HLists).

Here we write marshallers for HLists and Coproducts 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 vals 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.

Self Type
FamilyFormats with StandardFormats
Linear Supertypes
LowPriorityFamilyFormats, JsonFormatHints, AnyRef, Any
Known Subclasses
Ordering
  1. Alphabetic
  2. By Inheritance
Inherited
  1. FamilyFormats
  2. LowPriorityFamilyFormats
  3. JsonFormatHints
  4. AnyRef
  5. Any
  1. Hide All
  2. Show All
Visibility
  1. Public
  2. All

Type Members

  1. abstract class WrappedRootJsonFormat[Wrapped, SubRepr] extends AnyRef

    a JsonFormat[HList] or JsonFormat[Coproduct] would not retain the type information for the full generic that it is serialising.

    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 CoproductHints 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.

    Definition Classes
    LowPriorityFamilyFormats
  2. abstract class WrappedRootJsonFormatWithDefault[Wrapped, SubRepr, DefaultRepr] extends (LowPriorityFamilyFormats.this)#WrappedRootJsonFormat[Wrapped, SubRepr]

    Subclass of the the WrappedRootJsonFormat that provide a way to deserialize product with a default value.

    Subclass of the the WrappedRootJsonFormat that provide a way to deserialize product with a default value.

    Definition Classes
    LowPriorityFamilyFormats
  3. trait CoproductHint [T] extends AnyRef
    Definition Classes
    JsonFormatHints
  4. class FlatCoproductHint [T] extends CoproductHint[T]

    Product types are disambiguated by a {"key":"value",...}.

    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.

    Definition Classes
    JsonFormatHints
  5. sealed trait JsNullBehaviour extends AnyRef

    Sometimes the wire format needs to match an existing format and JsNull behaviour needs to be customised.

    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.

    Definition Classes
    JsonFormatHints
  6. class NestedCoproductHint [T] extends CoproductHint[T]

    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.

    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).

    Definition Classes
    JsonFormatHints
  7. trait ProductHint [T] extends AnyRef
    Definition Classes
    JsonFormatHints

Value Members

  1. final def !=(arg0: Any): Boolean
    Definition Classes
    AnyRef → Any
  2. final def ##(): Int
    Definition Classes
    AnyRef → Any
  3. final def ==(arg0: Any): Boolean
    Definition Classes
    AnyRef → Any
  4. final def asInstanceOf[T0]: T0
    Definition Classes
    Any
  5. implicit def cNilFormat[Wrapped](implicit t: Typeable[Wrapped]): (FamilyFormats.this)#WrappedRootJsonFormat[Wrapped, CNil]
    Definition Classes
    LowPriorityFamilyFormats
  6. def clone(): AnyRef
    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  7. implicit def coproductFormat[Wrapped, Name <: Symbol, Instance, Remaining <: Coproduct](implicit tpe: Typeable[Wrapped], th: (FamilyFormats.this)#CoproductHint[Wrapped], key: Aux[Name], jfh: Lazy[RootJsonFormat[Instance]], jft: (FamilyFormats.this)#WrappedRootJsonFormat[Wrapped, Remaining]): (FamilyFormats.this)#WrappedRootJsonFormat[Wrapped, :+:[FieldType[Name, Instance], Remaining]]
    Definition Classes
    LowPriorityFamilyFormats
  8. implicit def coproductHint[T](implicit arg0: Typeable[T]): (FamilyFormats.this)#CoproductHint[T]
    Definition Classes
    JsonFormatHints
  9. final def eq(arg0: AnyRef): Boolean
    Definition Classes
    AnyRef
  10. def equals(arg0: Any): Boolean
    Definition Classes
    AnyRef → Any
  11. implicit def familyFormat[T, Repr](implicit gen: Aux[T, Repr], sg: Cached[Strict[(FamilyFormats.this)#WrappedRootJsonFormat[T, Repr]]]): RootJsonFormat[T]

    Format for LabelledGenerics that uses the Coproduct marshaller above.

    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.

    Definition Classes
    LowPriorityFamilyFormats
  12. implicit def familyFormatWithDefault[T, Repr, DefaultRepr <: HList](implicit gen: Aux[T, Repr], default: Aux[T, DefaultRepr], sg: Cached[Strict[(FamilyFormats.this)#WrappedRootJsonFormatWithDefault[T, Repr, DefaultRepr]]]): RootJsonFormat[T]

    Format for LabelledGenerics that uses the HList marshaller below.

    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.

  13. def finalize(): Unit
    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( classOf[java.lang.Throwable] )
  14. final def getClass(): Class[_]
    Definition Classes
    AnyRef → Any
  15. implicit def hListFormat[Wrapped, Key <: Symbol, Value, Remaining <: HList, D <: HList](implicit t: Typeable[Wrapped], ph: (FamilyFormats.this)#ProductHint[Wrapped], key: Aux[Key], jfh: Lazy[JsonFormat[Value]], jft: (FamilyFormats.this)#WrappedRootJsonFormatWithDefault[Wrapped, Remaining, D]): (FamilyFormats.this)#WrappedRootJsonFormatWithDefault[Wrapped, ::[FieldType[Key, Value], Remaining], ::[Option[Value], D]]
    Definition Classes
    LowPriorityFamilyFormats
  16. implicit def hNilFormat[Wrapped](implicit t: Typeable[Wrapped]): (FamilyFormats.this)#WrappedRootJsonFormatWithDefault[Wrapped, HNil, HNil]
    Definition Classes
    LowPriorityFamilyFormats
  17. def hashCode(): Int
    Definition Classes
    AnyRef → Any
  18. final def isInstanceOf[T0]: Boolean
    Definition Classes
    Any
  19. final def ne(arg0: AnyRef): Boolean
    Definition Classes
    AnyRef
  20. final def notify(): Unit
    Definition Classes
    AnyRef
  21. final def notifyAll(): Unit
    Definition Classes
    AnyRef
  22. implicit def optionFormat[T](implicit arg0: JsonFormat[T]): JsonFormat[Option[T]]
  23. implicit def productHint[T](implicit arg0: Typeable[T]): (FamilyFormats.this)#ProductHint[T]
    Definition Classes
    JsonFormatHints
  24. final def synchronized[T0](arg0: ⇒ T0): T0
    Definition Classes
    AnyRef
  25. def toString(): String
    Definition Classes
    AnyRef → Any
  26. final def wait(): Unit
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  27. final def wait(arg0: Long, arg1: Int): Unit
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  28. final def wait(arg0: Long): Unit
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  29. object AlwaysJsNull extends JsNullBehaviour with Product with Serializable

    All values serialising to JsNull will be included in the wire format.

    All values serialising to JsNull will be included in the wire format. Ambiguous.

    Definition Classes
    JsonFormatHints
  30. object AlwaysJsNullTolerateAbsent extends JsNullBehaviour with Product with Serializable

    Same as AlwaysJsNull when serialising, with missing values treated as optional upon deserialisation.

    Same as AlwaysJsNull when serialising, with missing values treated as optional upon deserialisation. Ambiguous.

    Definition Classes
    JsonFormatHints
  31. object JsNullNotNone extends JsNullBehaviour with Product with Serializable

    Option values of None are omitted, but Some values of JsNull are retained.

    Option values of None are omitted, but Some values of JsNull are retained. Default.

    Definition Classes
    JsonFormatHints
  32. object NeverJsNull extends JsNullBehaviour with Product with Serializable

    No values serialising to JsNull will be included in the wire format.

    No values serialising to JsNull will be included in the wire format. Ambiguous.

    Definition Classes
    JsonFormatHints
  33. object UseDefaultJsNull extends JsNullBehaviour with Product with Serializable

    Use the case class default value provided for the field when available.

    Use the case class default value provided for the field when available. Ambiguous.

    Definition Classes
    JsonFormatHints

Inherited from LowPriorityFamilyFormats

Inherited from JsonFormatHints

Inherited from AnyRef

Inherited from Any

Ungrouped