Search code examples
jsonscalaserializationscala-3circe

What's the easiest way to serialize an opaque type with circe


Example:

opaque type UserName = String

This version is serialized automatically:

case class UserName(value: String) extends AnyVal

Solution

  • The easiest way is to NOT use raw opaque type:

    1. use Monix Newtypes, Iron, Neotype, Refined4s, ...
    2. these libraries have integrations e.g. for Circe:
    1. if they do not have an integration for a particular library... then they are exposing type classes which allow converting type classes for underlying types into wrapper types

    The mechanism for all of them is the same:

    type MyType = MyType.Type
    object MyType {
      opaque type Type = UnderlyingType
      // here code knows that Type = UnderlyingType
    
      // factories, extension methods, instances
    }
    
    // here code asking for MyType, resolves it to MyType.Type, then implicit
    // resolution would look inside object MyType for implicits
    

    it's just the common content is extracted into a mixin trait

    type MyType = MyType.Type
    object MyType extends Newtype[UnderlyingType] {
      // custom stuff
    }
    

    which would provide some instance of ConvertToAndFrom[Inner, Outer] (sometimes split into 2 type classes, 1 for extraction and 1 for construction, details depends on the library).

    It saves unnecessary burden of writing something like:

    // givens
    object namespace {
      opaque type MyType = String
      // opaque type (just like normal type alias)
      // CANNOT have a companion object, so putting implicits/givens into
      // object MyType will NOT automatically import them.
      // (Meaning you'd have to import MyType.given every time you need instances.)
      //
      // BUT putting things into top level `object` WILL pull implicits
      // in this object into implicit scope for opaque type defined in the same object.
      // Which is a trick used by all "newtypes| libraries.
      given Encoder[MyType] = Encoder.encodeString
      given DecoderMyType]  = Decoder.decodeString
    }