Injection: Creating Custom Encoders

Injection lets us define encoders for types that do not have one by injecting A into an encodable type B. This is the definition of the injection typeclass:

trait Injection[A, B] extends Serializable {
  def apply(a: A): B
  def invert(b: B): A
}

Example

Let's define a simple case class:

case class Person(age: Int, birthday: java.util.Calendar)
val people = Seq(Person(42, new java.util.GregorianCalendar()))
// people: Seq[Person] = List(
//   Person(
//     42,
//     java.util.GregorianCalendar[time=1711202985788,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Etc/UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2024,MONTH=2,WEEK_OF_YEAR=12,WEEK_OF_MONTH=4,DAY_OF_MONTH=23,DAY_OF_YEAR=83,DAY_OF_WEEK=7,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=2,HOUR_OF_DAY=14,MINUTE=9,SECOND=45,MILLISECOND=788,ZONE_OFFSET=0,DST_OFFSET=0]
//   )
// )

And an instance of a TypedDataset:

val personDS = TypedDataset.create(people)
// error: could not find implicit value for parameter encoder: frameless.TypedEncoder[repl.MdocSession.MdocApp0.Person]
// val personDS = TypedDataset.create(people)
//                ^^^^^^^^^^^^^^^^^^^^^^^^^^^

Looks like we can't, a TypedEncoder instance of Person is not available, or more precisely for java.util.Calendar. But we can define a injection from java.util.Calendar to an encodable type, like Long:

import java.util.Calendar

import frameless._

implicit val calendarToLongInjection = new Injection[Calendar, Long] {
  def apply(d: Calendar): Long = d.getTime.getTime

  def invert(l: Long): Calendar = {
    val cal = new java.util.GregorianCalendar()
    cal.setTime(new java.util.Date(l))
    cal
  }
}
// calendarToLongInjection: AnyRef with Injection[Calendar, Long] = repl.MdocSession$MdocApp0$$anon$1@70c61909

We can be less verbose using the Injection.apply function:

import frameless._

import java.util.Calendar

implicit val calendarToLongInjection = Injection[Calendar, Long](
  (_: Calendar).getTime.getTime,
  { (l: Long) =>
    val cal = new java.util.GregorianCalendar()
    cal.setTime(new java.util.Date(l))
    cal
  })
// calendarToLongInjection: Injection[Calendar, Long] = frameless.Injection$$anon$1@7d4fe061

Now we can create our TypedDataset:

val personDS = TypedDataset.create(people)
// personDS: TypedDataset[Person] = [age: int, birthday: bigint]

Another example

Let's define a sealed family:

sealed trait Gender
case object Male extends Gender
case object Female extends Gender
case object Other extends Gender

And a simple case class:

case class Person(age: Int, gender: Gender)
val people = Seq(Person(42, Male))
// people: Seq[Person] = List(Person(42, Male))

Again if we try to create a TypedDataset, we get a compilation error.

val personDS = TypedDataset.create(people)
// error: could not find implicit value for parameter encoder: frameless.TypedEncoder[repl.MdocSession.MdocApp1.Person]
// val personDS = TypedDataset.create(people)
//                ^^^^^^^^^^^^^^^^^^^^^^^^^^^

Let's define an injection instance for Gender:

import frameless._

implicit val genderToInt: Injection[Gender, Int] = Injection(
  {
    case Male   => 1
    case Female => 2
    case Other  => 3
  },
  {
    case 1 => Male
    case 2 => Female
    case 3 => Other
  })
// genderToInt: Injection[Gender, Int] = frameless.Injection$$anon$1@6647fa98

And now we can create our TypedDataset:

val personDS = TypedDataset.create(people)
// personDS: TypedDataset[Person] = [age: int, gender: int]

Alternatively, an injection instance can be derived for sealed families such as Gender using the following import, import frameless.TypedEncoder.injections._. This will encode the data constructors as strings.

Known issue: An invalid injection instance will be derived if there are data constructors with the same name. For example, consider the following sealed family:

sealed trait Foo
object A { case object Bar extends Foo }
object B { case object Bar extends Foo }

A.Bar and B.Bar will both be encoded as "Bar" thereby breaking the law that invert(apply(x)) == x.