Example:
opaque type UserName = String
This version is serialized automatically:
case class UserName(value: String) extends AnyVal
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(_)}
The easiest way is to NOT use raw opaque type
:
- use Monix Newtypes, Iron, Neotype, Refined4s, …
- these libraries have integrations e.g. for Circe:
- https://newtypes.monix.io/docs/circe.html
- https://iltotore.github.io/iron/docs/modules/circe.html
- https://github.com/kitlangton/neotype?tab=readme-ov-file#integrations
- https://refined4s.kevinly.dev/docs/circe/
- 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
- https://newtypes.monix.io/docs/core.html#encoders-and-decoders
- https://iltotore.github.io/iron/docs/reference/newtypes.html#typeclass-derivation-1
- https://github.com/kitlangton/neotype/blob/65eafb48175587448d74db449d1808ee09847fdc/modules/core/shared/src/main/scala/neotype/package.scala#L106
- https://refined4s.kevinly.dev/docs/circe/#with-deriving-method
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
}