If you want to restrict this to only working on single argument case-classes, then you cannot use ProductTypeClass
. The purpose of it is to generalize inductively to all products of arbitrary arity. You want to stick to arity 1, so you have a conflict. You just have to write everything yourself without the (small) boilerplate ProductTypeclass
abstracts for you.
object ShapelessJsonOFormat {
implicit def caseHListArity1[Head](implicit
headFmt: Lazy[OFormat[Head]] // Use Lazy in some places to get around the (incorrect) implicit divergence check in the compiler
): OFormat[Head :: HNil] = new OFormat[Head :: HNil] {
// ... serialize a Head :: HNil given headFmt.value: OFormat[Head]
}
implicit def caseGeneric[I, O](implicit
gen: Generic.Aux[I, O],
oFmt: Lazy[OFormat[O]]
): OFormat[I] = new OFormat[I] {
// ... serialize an I given a Generic I => O and oFmt.value: OFormat[O]
}
}
From the looks of it, your major complaint is that you need to hack together multiple JSONObject
s in the arity-abstracted version. This is because you are using Generic
, not LabelledGeneric
, and so you don't have the ability to give field names to the elements of the product, so they get mushed together on one level. You can normally use LabelledProductTypeClass
for that, but it doesn't quite work here, so we're stuck with our own boilerplate. This version works on any arity.
type OFormatObject[A] = OFormat[A] { def write(value: A): JsObject }
object ShapelessJsonOFormat {
implicit val caseHNil: OFormatObject[HNil] = new OFormat[HNil] {
override def read(json: JsValue) = HNil
override def write(value: HNil) = JsObject.empty
}
implicit def caseHCons[
HeadName <: Symbol,
Head,
Tail <: HList
](implicit
headFmt: Lazy[OFormat[Head]],
nameWitness: Witness.Aux[HeadName],
tailFmt: Lazy[OFormatObject[Tail]]
): OFormatObject[FieldType[HeadName, Head] :: Tail]
= new OFormat[FieldType[HeadName, Head] :: Tail] {
private val fieldName = nameWitness.value.name // Witness[_ <: Symbol] => Symbol => String
override def read(json: JsValue): FieldType[HeadName, Head] :: Tail = {
val headObj = json.asJsObject.get(fieldName)
val head = headFmt.read(headObj)
val tail = tailFmt.read(json)
field[HeadName](head) :: tail
}
override def write(value: FieldType[HeadName, Head] :: Tail): JsObject = {
val tail = tailFmt.write(value.tail)
val head = headFmt.write(value.head)
tail + JsObject(fieldName -> head) // or similar
}
}
implicit def caseLabelledGeneric[I, O](implicit
gen: LabelledGeneric.Aux[I, O],
oFmt: Lazy[OFormatObject[O]]
): OFormatObject[I] = new OFormat[I] {
override def read(json: JsValue): I = gen.from(oFmt.value.read(json))
override def write(value: I): JsObject = oFmt.value.write(gen.to(value))
}
}
The idea here is to use the refinement OFormatObject
to talk about OFormat
s that are guaranteed to write
JsObject
s. There is an OFormatObject[HNil]
that's just { read = _ => HNil; write = _ => {} }
. If there's a serializer for Head
(OFormat[Head]
), an object serializer for Tail
(OFormatObject[Tail]
), and we have some singleton type that represents the field name of Head
(type parameter HeadName <: Symbol
, realized in Witness.Aux[HeadName]
), then caseHCons
will produce a OFormatObject[FieldName[HeadName, Head] :: Tail]
, which looks like { read = { headName: head, ..tail } => head :: tail; write = head :: tail => { headName: head, ..tail }
. We then use caseLabelledGeneric
to bring the solution for HList
s-with-FieldType
s into general case classes.
override object typeClass
, which is cleaner. – HTNW