Search code examples
jsonscalashapelesscase-class

Shapeless. Refer to all case classes of 1 constructor argument


I am trying to generalize over all case classes of 1 argument constructor to create Json serializers for them using shapeless.

What I have done so far is:

object ShapelessJsonOFormat extends ProductTypeClassCompanion[OFormat] {
  object typeClass extends ProductTypeClass[OFormat] {
    override def product[H, T <: HList](ch: OFormat[H], ct: OFormat[T]): OFormat[H :: T] = new OFormat[H :: T] {
      override def reads(json: JsValue): JsResult[H :: T] =
        for {
          h <- ch.reads(json)
          t <- ct.reads(json)
        } yield h :: t


      override def writes(o: H :: T): JsObject = ch.writes(o.head) ++ ct.writes(o.tail)
    }

    override def emptyProduct: OFormat[HNil] = new OFormat[HNil] {

      override def reads(json: JsValue): JsResult[HNil] = JsSuccess(HNil)

      override def writes(o: HNil): JsObject = JsObject.empty
    }

    override def project[F, G](
      instance: => OFormat[G],
      to: (F) => G,
      from: (G) => F): OFormat[F] = new OFormat [F] {
      override def writes(o: F): JsObject = instance.writes(to(o))

      override def reads(json: JsValue): JsResult[F] = instance.reads(json).map(from)
    }
  }
}

I have not tried it but it is not really what I want. The problem is that extending ProductTypeClassCompanion implies generalizing over all HLists so to be able to "add" two jsons I am forced here to play with JsonObjects (see the ++ in override def writes(o: H :: T): JsObject = ch.writes(o.head) ++ ct.writes(o.tail)). What I really need is to generalize over all HList of type T :: HNil. Is this possible? Otherwise I am enforced to make all my case classes extend Product1[T].


Solution

  • 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 JSONObjects 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 OFormats that are guaranteed to write JsObjects. 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 HLists-with-FieldTypes into general case classes.