0

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?

foxtrotuniform6969
  • 3,527
  • 7
  • 28
  • 54

1 Answers1

2

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.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • Thank you for your excellent answer! I had a feeling it had to do with my weak understanding of inheritance in Scala. That did indeed work, but now it is the responsibility of classes that inherit from `RecordObject` to implement ID generation. I would prefer this to be automatic and for the ID to be unchangeable once the class is instantiated. Is there a way to do this? – foxtrotuniform6969 Apr 25 '20 at 19:10
  • 1
    You _have_ to define a constructor allowing you to override `_id` - how else can you recreate an instance with specific ID? You could however create a `protected` method `generateID` in `RecordObject` and overload constructor to use it: `def this(name: String) = this(name, generateID)`. You will never have a certainty that user extending your interface would use it though. Which wouldn't make sense to constrain as you still need to pass arbitrary `String` from outside world in order to recreate a value. That string could be get broken at any point before it reaches constructor. – Mateusz Kubuszok Apr 25 '20 at 19:16
  • Hmm I see your point. So what would be a good way to create some type of object with an auto-generated property *as a default value* that other objects can extend to get that property, without those extenders doing the work? Is that even good practice? – foxtrotuniform6969 Apr 25 '20 at 19:18
  • 1
    What I would do would be simply creating a separate type to represent that `BSONObjectID`. This type would have two public methods to create itself, one which would parse `String` into something like `Either[Error, BSONObjectID]` and one method for createing a new ID. Then I would write manually codecs for that ID to make sure that if ID is broken - it fails Circe parsing. At this point I wouldn't have to fear that downstream can use IDs in a wrong way as there would be no possible way for them to do it. – Mateusz Kubuszok Apr 25 '20 at 19:21
  • 1
    Gotcha. Thank you so much for your help, your time, and your quick replies. I'm pleasantly surprised that the Scala community on SO seems to be livelier than it was even just a year ago. – foxtrotuniform6969 Apr 25 '20 at 19:24
  • One more follow-up question, any more reading to recommend on this subject? Not only Circe encoding, but also maybe a dive into Scala `trait`/`class`/`case class`/`abstract class` inheritance in general? – foxtrotuniform6969 Apr 25 '20 at 19:30
  • 1
    I guess looking up things like: typeclass derivation, making illegal states irreplaceable, ADTs and well, types and implicits in general wouldn't hurt. – Mateusz Kubuszok Apr 25 '20 at 19:51