3

I'd like to encode Array[Byte] fields of my case classes as Base64 strings. For some reason Circe doesn't pick up my codec using default one instead that converts byte array into json array of ints.

What should I do to fix it ? Here is my minimized code

import io.circe.generic.JsonCodec

sealed trait DocumentAttribute

@JsonCodec
sealed case class DAAudio(title: Option[String], performer: Option[String], waveform: Option[Array[Byte]], duration: Int) extends DocumentAttribute

@JsonCodec
sealed case class DAFilename(fileName: String) extends DocumentAttribute

object CirceEncodersDecoders {
  import io.circe._
  import io.circe.generic.extras._
  import io.circe.generic.extras.semiauto._

  implicit val arrayByteEncoder: Encoder[Array[Byte]] = Encoder.encodeString.contramap[Array[Byte]] { bytes ⇒
    Base64.getEncoder.encodeToString(bytes)
  }

  val printer: Printer = Printer.noSpaces.copy(dropNullValues = true, reuseWriters = true)
  implicit val config: Configuration = Configuration.default.withDiscriminator("kind").withSnakeCaseConstructorNames.withSnakeCaseMemberNames

  implicit val DocumentAttributeEncoder: Encoder[DocumentAttribute] = deriveEncoder
  implicit val DocumentAttributeDecoder: Decoder[DocumentAttribute] = deriveDecoder
}

object main {
  def main(args: Array[String]): Unit = {
    import CirceEncodersDecoders._

    import io.circe.parser._
    import io.circe.syntax._

    val attributes: List[DocumentAttribute] = List(
      DAAudio(Some("title"), Some("perform"), Some(List(1, 2, 3, 4, 5).map(_.toByte).toArray), 15),
      DAFilename("filename")
    )
    val j2 = attributes.asJson
    val decoded2 = decode[List[DocumentAttribute]](j2.noSpaces)
    println(decoded2)
  }
}
expert
  • 29,290
  • 30
  • 110
  • 214

2 Answers2

1

When you do this:

implicit val DocumentAttributeEncoder: Encoder[DocumentAttribute] = deriveEncoder

circe tries to get suitable Encoder for DAFilename and DAAudio. However, since those already exist (by means of @JsonCodec on individual classes), it does not re-derive them from scratch using generics and your Encoder[Array[Byte]] at scope - which you want.

So you can either get rid of @JsonCodec (so it auto-derives codecs for DAFilename and DAAudio together with DocumentAttribute) or trigger re-derivation manually:

implicit val AudioDecoder: Encoder[DAAudio] = deriveEncoder // takes priority over existing one
implicit val DocumentAttributeEncoder: Encoder[DocumentAttribute] = deriveEncoder // AudioDecoder will be used here

You also need to build a Decoder for Array[Byte] and repeat the process above for Decoders, otherwise it will try to parse Base64 string as a list of ints, resulting in a failure.

Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
0

It seams that @JsonCodec annotations don't work with a custom encoder for Array[Byte].

Here is all the stuff that need for encoding and decoding of your classes with circe:

object CirceEncodersDecoders2 {
  val printer: Printer = Printer.noSpaces.copy(dropNullValues = true, reuseWriters = true)
  implicit val arrayByteEncoder: Encoder[Array[Byte]] =
    Encoder.encodeString.contramap[Array[Byte]](Base64.getEncoder.encodeToString)
  implicit val arrayByteDecoder: Decoder[Array[Byte]] =
    Decoder.decodeString.map[Array[Byte]](Base64.getDecoder.decode)
  implicit val config: Configuration = Configuration.default.withDiscriminator("kind").withSnakeCaseConstructorNames.withSnakeCaseMemberNames
  implicit val audioEncoder: Encoder[DAAudio] = deriveEncoder[DAAudio]
  implicit val audioDecoder: Decoder[DAAudio] = deriveDecoder[DAAudio]
  implicit val filenameEncoder: Encoder[DAFilename] = deriveEncoder[DAFilename]
  implicit val filenameDecoder: Decoder[DAFilename] = deriveDecoder[DAFilename]
  implicit val documentAttributeEncoder: Encoder[DocumentAttribute] = deriveEncoder[DocumentAttribute]
  implicit val documentAttributeDecoder: Decoder[DocumentAttribute] = deriveDecoder[DocumentAttribute]
}

If you are not limited in selection of JSON parser/serializer, then you can try a more efficient solution using jsoniter-scala.

DISCLAIMER: I'm an author of this library.

Here are results of benchmarks for both implementations:

[info] Benchmark                                        Mode  Cnt        Score           Error   Units
[info] ListOfAdtWithBase64Benchmark.readCirce           thrpt 5   114927.343 ±   7910.068 ops/s
[info] ListOfAdtWithBase64Benchmark.readJsoniterScala   thrpt 5  1818299.170 ± 162757.404 ops/s
[info] ListOfAdtWithBase64Benchmark.writeCirce          thrpt 5   117982.635 ±   8942.816 ops/s
[info] ListOfAdtWithBase64Benchmark.writeJsoniterScala  thrpt 5  4281752.461 ± 319953.287 ops/s

Full sources are here.

Andriy Plokhotnyuk
  • 7,883
  • 2
  • 44
  • 68
  • `@JsonCodec` annotations indeed do work with custom decoders, as long as those are available in scope at the moment of derivation, as do `deriveEncoder` and `deriveDecoder`. Your code is redundant since you don't need to manually derive instances for all the subtypes of the sealed trait nor do you need to pass type parameter to `deriveXXX` explicitly. – Oleg Pyzhcov Mar 01 '18 at 13:37
  • That, and you advertise your own library without explicit disclaimer, assuming OP is looking for speed (while there are also other reasons to be using circe, like optics, yaml support, http4s and/or fs2 integration), which is mentioned nowhere, with a benchmark results that take half of the answer instead of a code of how the problem would actually be solved. – Oleg Pyzhcov Mar 01 '18 at 13:40