david.perez
david.perez

Reputation: 7012

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

Upvotes: 1

Views: 135

Answers (3)

St.
St.

Reputation: 533

If you use a trick like

opaque type UserName <: String = String

you can define codecs like

given Encoder[UserName] = Encoder[String].contramap(UserName.apply)
given Decoder[UserName] = Decoder[String].map(UserName.apply)

For simplicity I define

inline def opaqueTypeCodec[Base : Encoder: Decoder, Opaque <: Base](apply: Base => Opaque): Codec[Opaque] =
  Codec.from(Decoder[Base].map(apply), Encoder[Base].contramap[Opaque](apply))

and then

given Codec[UserName] = opaqueTypeCodec[String, UserName](UserName.apply)

Upvotes: 1

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27535

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
}

Upvotes: 2

david.perez
david.perez

Reputation: 7012

I don't know if it is the most elegant way of doing it, because I'm novice in circe:

opaque type UserName = String

object UserName:
  def apply(s: String): UserName = new UserName(s)
  given Encoder[UserName] = new Encoder[UserName]:
    def apply(a: UserName): Json = Json.fromString(a.toString)
  given Decoder[UserName] = new Decoder[UserName]:
    def apply(c: HCursor): Decoder.Result[UserName] =
      c.as[String].map{ UserName(_)}

Upvotes: 1

Related Questions