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
- Alphabetic
- By Inheritance
- FamilyFormats
- LowPriorityFamilyFormats
- JsonFormatHints
- AnyRef
- Any
- Hide All
- Show All
- Public
- All
Type Members
-
abstract
class
WrappedRootJsonFormat[Wrapped, SubRepr] extends AnyRef
a
JsonFormat[HList]orJsonFormat[Coproduct]would not retain the type information for the full generic that it is serialising.a
JsonFormat[HList]orJsonFormat[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) customCoproductHints on a per-trait level 2) configurablenullbehaviour on a per product level 3) clearer error messages.This is intentionally not part of the
JsonFormathierarchy to avoid ambiguous implicit errors.- Definition Classes
- LowPriorityFamilyFormats
-
abstract
class
WrappedRootJsonFormatWithDefault[Wrapped, SubRepr, DefaultRepr] extends (LowPriorityFamilyFormats.this)#WrappedRootJsonFormat[Wrapped, SubRepr]
Subclass of the the
WrappedRootJsonFormatthat provide a way to deserialize product with a default value.Subclass of the the
WrappedRootJsonFormatthat provide a way to deserialize product with a default value.- Definition Classes
- LowPriorityFamilyFormats
-
trait
CoproductHint
[T] extends AnyRef
- Definition Classes
- JsonFormatHints
-
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
-
sealed
trait
JsNullBehaviour
extends AnyRef
Sometimes the wire format needs to match an existing format and
JsNullbehaviour needs to be customised.Sometimes the wire format needs to match an existing format and
JsNullbehaviour needs to be customised. This allows null behaviour to be defined at the product level. Field level control is only possible with a user-definedRootJsonFormat.- Definition Classes
- JsonFormatHints
-
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
-
trait
ProductHint
[T] extends AnyRef
- Definition Classes
- JsonFormatHints
Value Members
-
final
def
!=(arg0: Any): Boolean
- Definition Classes
- AnyRef → Any
-
final
def
##(): Int
- Definition Classes
- AnyRef → Any
-
final
def
==(arg0: Any): Boolean
- Definition Classes
- AnyRef → Any
-
final
def
asInstanceOf[T0]: T0
- Definition Classes
- Any
-
implicit
def
cNilFormat[Wrapped](implicit t: Typeable[Wrapped]): (FamilyFormats.this)#WrappedRootJsonFormat[Wrapped, CNil]
- Definition Classes
- LowPriorityFamilyFormats
-
def
clone(): AnyRef
- Attributes
- protected[java.lang]
- Definition Classes
- AnyRef
- Annotations
- @throws( ... )
-
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
-
implicit
def
coproductHint[T](implicit arg0: Typeable[T]): (FamilyFormats.this)#CoproductHint[T]
- Definition Classes
- JsonFormatHints
-
final
def
eq(arg0: AnyRef): Boolean
- Definition Classes
- AnyRef
-
def
equals(arg0: Any): Boolean
- Definition Classes
- AnyRef → Any
-
implicit
def
familyFormat[T, Repr](implicit gen: Aux[T, Repr], sg: Cached[Strict[(FamilyFormats.this)#WrappedRootJsonFormat[T, Repr]]]): RootJsonFormat[T]
Format for
LabelledGenericsthat uses theCoproductmarshaller above.Format for
LabelledGenericsthat uses theCoproductmarshaller 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 listg: LabelledGeneric[T], f: Cached[Strict[JsonFormat[T.Repr]]]but that's not possible.- Definition Classes
- LowPriorityFamilyFormats
-
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
LabelledGenericsthat uses theHListmarshaller below.Format for
LabelledGenericsthat uses theHListmarshaller 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 listg: LabelledGeneric[T], f: Cached[Strict[JsonFormat[T.Repr]]]but that's not possible. -
def
finalize(): Unit
- Attributes
- protected[java.lang]
- Definition Classes
- AnyRef
- Annotations
- @throws( classOf[java.lang.Throwable] )
-
final
def
getClass(): Class[_]
- Definition Classes
- AnyRef → Any
-
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
-
implicit
def
hNilFormat[Wrapped](implicit t: Typeable[Wrapped]): (FamilyFormats.this)#WrappedRootJsonFormatWithDefault[Wrapped, HNil, HNil]
- Definition Classes
- LowPriorityFamilyFormats
-
def
hashCode(): Int
- Definition Classes
- AnyRef → Any
-
final
def
isInstanceOf[T0]: Boolean
- Definition Classes
- Any
-
final
def
ne(arg0: AnyRef): Boolean
- Definition Classes
- AnyRef
-
final
def
notify(): Unit
- Definition Classes
- AnyRef
-
final
def
notifyAll(): Unit
- Definition Classes
- AnyRef
- implicit def optionFormat[T](implicit arg0: JsonFormat[T]): JsonFormat[Option[T]]
-
implicit
def
productHint[T](implicit arg0: Typeable[T]): (FamilyFormats.this)#ProductHint[T]
- Definition Classes
- JsonFormatHints
-
final
def
synchronized[T0](arg0: ⇒ T0): T0
- Definition Classes
- AnyRef
-
def
toString(): String
- Definition Classes
- AnyRef → Any
-
final
def
wait(): Unit
- Definition Classes
- AnyRef
- Annotations
- @throws( ... )
-
final
def
wait(arg0: Long, arg1: Int): Unit
- Definition Classes
- AnyRef
- Annotations
- @throws( ... )
-
final
def
wait(arg0: Long): Unit
- Definition Classes
- AnyRef
- Annotations
- @throws( ... )
-
object
AlwaysJsNull
extends JsNullBehaviour with Product with Serializable
All values serialising to
JsNullwill be included in the wire format.All values serialising to
JsNullwill be included in the wire format. Ambiguous.- Definition Classes
- JsonFormatHints
-
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
-
object
JsNullNotNone
extends JsNullBehaviour with Product with Serializable
Option values of
Noneare omitted, butSomevalues ofJsNullare retained.Option values of
Noneare omitted, butSomevalues ofJsNullare retained. Default.- Definition Classes
- JsonFormatHints
-
object
NeverJsNull
extends JsNullBehaviour with Product with Serializable
No values serialising to
JsNullwill be included in the wire format.No values serialising to
JsNullwill be included in the wire format. Ambiguous.- Definition Classes
- JsonFormatHints
-
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