Reputation: 7012
Example:
opaque type UserName = String
This version is serialized automatically:
case class UserName(value: String) extends AnyVal
Upvotes: 1
Views: 135
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
Reputation: 27535
The easiest way is to NOT use raw opaque type
:
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
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