scalaavro4s

Implicit object works inline but not when it is imported


I am using avro4s to help with avro serialization and deserialization.

I have a case class that includes Timestamps and need those Timestamps to be converted to nicely formatted strings before I publish the records to Kafka; the default encoder is converting my Timestamps to Longs. I read that I needed to write a decoder and encoder (from the avro4s readme).

Here is my case class:

case class MembershipRecordEvent(id: String,
                                 userHandle: String,
                                 planId: String,
                                 teamId: Option[String] = None,
                                 note: Option[String] = None,
                                 startDate: Timestamp,
                                 endDate: Option[Timestamp] = None,
                                 eventName: Option[String] = None,
                                 eventDate: Timestamp)

I have written the following encoder:

Test.scala

def test() = {
implicit object MembershipRecordEventEncoder extends Encoder[MembershipRecordEvent] {
  override def encode(t: MembershipRecordEvent, schema: Schema) = {
    val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
    val record = new GenericData.Record(schema)
    record.put("id", t.id)
    record.put("userHandle", t.userHandle)
    record.put("teamId", t.teamId.orNull)
    record.put("note", t.note.orNull)
    record.put("startDate", dateFormat.format(t.startDate))
    record.put("endDate", if(t.endDate.isDefined) dateFormat.format(t.endDate.get) else null)
    record.put("eventName", t.eventName.orNull)
    record.put("eventDate", dateFormat.format(t.eventDate))
    record
    }
  }

val recordInAvro2 = Encoder[MembershipRecordEvent].encode(testRecord, AvroSchema[MembershipRecordEvent]).asInstanceOf[GenericRecord]
    println(recordInAvro2)
}

If I declare the my implicit object in line, like I did above, it creates the GenericRecord that I am looking for just fine. I tried to abstract the implicit object to a file, wrapped in an object, and I import Implicits._ to use my custom encoder.

Implicits.scala

object Implicits {
implicit object MembershipRecordEventEncoder extends Encoder[MembershipRecordEvent] {
  override def encode(t: MembershipRecordEvent, schema: Schema) = {
    val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
    val record = new GenericData.Record(schema)
    record.put("id", t.id)
    record.put("userHandle", t.userHandle)
    record.put("teamId", t.teamId.orNull)
    record.put("note", t.note.orNull)
    record.put("startDate", dateFormat.format(t.startDate))
    record.put("endDate", if(t.endDate.isDefined) dateFormat.format(t.endDate.get) else null)
    record.put("eventName", t.eventName.orNull)
    record.put("eventDate", dateFormat.format(t.eventDate))
    record
    }
  }
}

Test.scala

import Implicits._
val recordInAvro2 = Encoder[MembershipRecordEvent].encode(testRecord, AvroSchema[MembershipRecordEvent]).asInstanceOf[GenericRecord]
    println(recordInAvro2)

It fails to use my encoder (doesn't hit my breakpoints). I have tried a myriad of things to try and see why it fails to no avail.

How can I correctly import an implicit object?

Is there a simpler solution to encode my case class's Timestamps to Strings without writing an encoder for the entire case class?


Solution

  • TL;DR

    As suggested in one of the comments above, you can place it in the companion object.

    The longer version:

    Probably you have another encoder, that is used instead of the encoder you defined in Implicits.

    I'll quote some phrases from WHERE DOES SCALA LOOK FOR IMPLICITS?

    When a value of a certain name is required, lexical scope is searched for a value with that name. Similarly, when an implicit value of a certain type is required, lexical scope is searched for a value with that type.

    Any such value which can be referenced with its “simple” name, without selecting from another value using dotted syntax, is an eligible implicit value. There may be more than one such value because they have different names.

    In that case, overload resolution is used to pick one of them. The algorithm for overload resolution is the same used to choose the reference for a given name, when more than one term in scope has that name. For example, println is overloaded, and each overload takes a different parameter type. An invocation of println requires selecting the correct overloaded method.

    In implicit search, overload resolution chooses a value among more than one that have the same required type. Usually this entails selecting a narrower type or a value defined in a subclass relative to other eligible values.

    The rule that the value must be accessible using its simple name means that the normal rules for name binding apply.

    In summary, a definition for x shadows a definition in an enclosing scope. But a binding for x can also be introduced by local imports. Imported symbols can’t override definitions of the same name in an enclosing scope. Similarly, wildcard imports can’t override an import of a specific name, and names in the current package that are visible from other source files can’t override imports or local definitions.

    These are the normal rules for deciding what x means in a given context, and also determine which value x is accessible by its simple name and is eligible as an implicit.

    This means that an implicit in scope can be disabled by shadowing it with a term of the same name.

    Now I'll state the companion object logic:

    Implicit syntax can avoid the import tax, which of course is a “sin tax,” by leveraging “implicit scope”, which depends on the type of the implicit instead of imports in lexical scope.

    When an implicit of type T is required, implicit scope includes the companion object T: When an F[T] is required, implicit scope includes both the companion of F and the companion of the type argument, e.g., object C for F[C].

    In addition, implicit scope includes the companions of the base classes of F and C, including package objects, such as p for p.F.