Saskia
Saskia

Reputation: 1056

ConfiguredJsonCodec for snake case ADT

I have to consume a json api, where there is a list of discrete string values in snake case.

The example below works, but I would like to remove the manual fooDecoder in favor of the (currently commented out) ConfiguredJsonCodec annotation.

Or more fundamental question: Does modeling these discrete values as case objects in an ADT even makes sense, or is there another approach?

import io.circe._
import io.circe.syntax._
import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec, JsonKey}
import io.circe.parser.parse

implicit val jsonConfig: Configuration = Configuration.default
  .withSnakeCaseConstructorNames
  .withSnakeCaseMemberNames

//@ConfiguredJsonCodec(decodeOnly = true)
sealed trait Foo
object Foo {
  case object FooBar extends Foo
  case object FooBaz extends Foo
  case object FooWuz extends Foo
}

import Foo._
implicit val fooDecoder: Decoder[Foo] = new Decoder[Foo] {
  override def apply(c: HCursor) = c.as[String].map{
    case "foo_bar" => FooBar
    case "foo_baz" => FooBaz
    case "foo_wuz" => FooWuz
  }
}

@ConfiguredJsonCodec(decodeOnly = true)
case class Qux(fooFoo: List[Foo])

val input ="""{"foo_foo" : ["foo_bar", "foo_baz", "foo_wuz"]}"""
val json: Json = parse(input).left.map(println(_)).right.get

json.as[Qux]

Complete example: https://scastie.scala-lang.org/eVFyNMGFRgaw9oEkRveT8g

This uses circe 0.13.0

Upvotes: 2

Views: 789

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27535

All Circe derivation methods for coproducts assume that there is discrimination field and at best you can pick its name. So the supported format is something like:

{
  "type": "foo_bar",
}

instead of just

"foo_bar"

The reason is that derivation mechanism sees no difference between case-object-only and case-object-and-case-classes, so for code like this

trait MyADT
object MyADT {
  case class Value1(value: String) extends MyADT
  case class Value2(value: String) extends MyADT
}

where you simply would combine codecs of sum's parts

val decoder: Decoder[MyADT] = Decoder[Value1].widen orElse Decoder[Value2].widen

you would always decode to first Value1 even if you encoded Value2. Derivation sees no difference between these two cases and so they prefer to be on the safe side always by using discrimination field. (This is discussed in docs).

However, if you know that you are building enum, you can simply use enumeratum with circe support:

import enumeratum._

sealed trait Foo extends EnumEntry with Snakecase
object Foo extends Enum[Foo] with CirceEnum[Foo] {
  case object FooBar extends Foo
  case object FooBaz extends Foo
  case object FooWuz extends Foo

  val values = findValues
}

This would achieve the very goal you aim for, although in a different way.

Upvotes: 2

Related Questions