Automatically create product/coproduct marshallers (i.e.
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.
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.
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.
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.