Search code examples
jsonscalacircehttp4s-circe

Circe asJson not encoding properties from abstract base class


Suppose I have the following abstract base class:

package Models

import reactivemongo.bson.BSONObjectID


abstract class RecordObject {
  val _id: String = BSONObjectID.generate().stringify
}

Which is extended by the following concrete case class:

package Models

case class PersonRecord(name: String) extends RecordObject

I then try to get a JSON string using some code like the following:

import io.circe.syntax._
import io.circe.generic.auto._
import org.http4s.circe._
// ...

val person = new PersonRecord(name = "Bob")
println(person._id, person.name) // prints some UUID and "Bob"
println(person.asJso) // {"name": "Bob"} -- what happened to "_id"? 

As you can see, the property _id: String inherited from RecordObject is missing. I would expect that the built-in Encoder should function just fine for this use case. Do I really need to build my own?


Solution

  • Let's see what happens in encoder generation. Circe uses shapeless to derive its codecs, so its enough to check what shapeless resolves into to answer your question. So in ammonite:

    @ abstract class RecordObject {
        val _id: String = java.util.UUID.randomUUID.toString
      }
    defined class RecordObject
    
    @ case class PersonRecord(name: String) extends RecordObject
    defined class PersonRecord
    
    @  import $ivy.`com.chuusai::shapeless:2.3.3`, shapeless._
    import $ivy.$                             , shapeless._
    
    @ Generic[PersonRecord]
    res3: Generic[PersonRecord]{type Repr = String :: shapeless.HNil} = ammonite.$sess.cmd3$anon$macro$2$1@1123d461
    

    OK, so its String :: HNil. Fair enough - what shapeless does is extracting all fields available in constructor transforming one way, and putting all fields back through constructor if converting the other.

    Basically all typeclass derivation works this way, so you should make it possible to pass _id as constructor:

    abstract class RecordObject {
        val _id: String
    }
    
    case class PersonRecord(
      name: String,
      _id: String = BSONObjectID.generate().stringify
    ) extends RecordObject
    

    That would help type class derivation do its work. If you cannot change how PersonRecord looks like... then yes you have to write your own codec. Though I doubt it would be easy as you made _id immutable and impossible to set from outside through a constructor, so it would also be hard to implement using any other way.