Circe encoder for generic ADT with context bound

100 Views Asked by At

I have the following setup:

  import io.circe.generic.extras.Configuration
  import io.circe.generic.extras.auto._
  import io.circe.syntax._

  implicit val Conf: Configuration = Configuration
    .default
    .withSnakeCaseConstructorNames
    .withSnakeCaseMemberNames
    .withDiscriminator("type")

  sealed trait Generator[T]
  case class UniformGenerator[T](min: T, max: T) extends Generator[T]
  case class EnumGenerator[T](values: Set[T]) extends Generator[T]

  sealed trait DataType
  case class IntType(generator: Generator[Int]) extends DataType
  case class StringType(generator: Generator[String]) extends DataType

Encoding these ADTs into JSON is working well, executing:

  println((IntType(UniformGenerator(1, 5)): DataType).asJson)

prints

{
  "generator" : {
    "min" : 1,
    "max" : 5,
    "type" : "uniform_generator"
  },
  "type" : "int_type"
}

However, when I want to restrict the generic type for one of the generators, for instance when I require the UniformGenerator to work only with Numeric types:

case class UniformGenerator[T: Numeric](min: T, max: T) extends Generator[T]

It fails with the following error:

could not find implicit value for parameter encoder: io.circe.Encoder[DataType] println((IntType(UniformGenerator(1, 5)): DataType).asJson)

So, it can't derive the right encoder automatically. What is the reason for that and how can be fixed?

2

There are 2 best solutions below

3
MartinHH On

Edit: turns out this answer is wrong (or: only true for Scala 3 which the original question is not about). I made a wrong assumption about what circe does and does not support. See answer and comments by @sanyi14ka.

A) Reason(s)

To understand the reason(s), you need to understand that

class UniformGenerator[T: Numeric](min: T, max: T) extends Generator[T]

is just syntactic sugar for:

class UniformGenerator[T](min: T, max: T)(implicit ev: Numeric[T]) extends Generator[T]

So by adding the context bound, you add an additional parameter in an additional parameter list.

a) Main Reason: derivation does not support more than one parameter lists

You could try this by changing your case class definition to

case class UniformGenerator[T](min: T)(max: T) extends Generator[T]

That would also fail to compile (if you try to derive an Encoder for it) because derivation only supports case classes with one parameter list.

b) Additional reason: circe would need an Encoder[Numeric[Int]]

If derivation would support additional parameter lists, circe would need an Encoder for the new parameter - but there is no Encoder[Numeric[Int]] in scope (nor could it be derived).

B) Workarounds

As far as I am aware, there is no magic "fix" for this, but I could propose some workarounds:

a) Do not add the context bound to the case class

Depending on your use case, it might be acceptable to not add the context bound to the case class and instead add it to the functions that need it, e.g.:

def doSometingWithNumericUniformGenerator[T: Numeric](generator: UniformGenerator[T]) = // ...

b) Implement the specific Encoder manually

(the obvious solution)

c) Move the "context bound" into the main parameter list and provide an Encoder for it

You could make the third parameter explicit:

class UniformGenerator[T](min: T, max: T, n: Numeric[T]) extends Generator[T]

By adding another apply method to the companion, you could still construct it as usual:

object UniformGenerator {
  def apply[T: Numeric](min: T, max: T): UniformGenerator[T] =
    UniformGenerator[T](min, max, implicitly[Numeric[T]])
}

Then, you would need to somehow provide an

implicit val numericIntEncoder: Encoder[Numeric[Int]] = ???

Once you have that, derivation of an Encoder[UniformGenerator[Int]] should work.

1
sanyi14ka On

I realized that circe wants to automatically and lazily derive an encoder for each possible ADT that can happen. If I restrict the generic type, then the Encoder for Generator[String] will fail. If we try to define it explicitly:

  implicit val stringGenEnc: Encoder[Generator[String]] = Encoder.instance {
    case UniformGenerator(min, max) => ???
    case EnumGenerator(values) => ???
  }

then it can't derive an encoder for UniformGenerator. However, we can define one encoder to help circe. For my use case it can be

  implicit val stringUnifGenEnc: Encoder[UniformGenerator[String]] = _ => Json.Null

and similar way one can define a decoder, too:

  implicit val stringUnifGenDec: Decoder[UniformGenerator[String]] = _ =>
    Left(DecodingFailure("uniform_generator for strings are not supported", List.empty))

Likewise, circe can automatically derive encoders and decoders for DataType, too.