Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decode case class, String or Int in circe

Tags:

json

scala

circe

I use some Rest API responding with json containing a sort of "mixed" field. By mixed I mean that it can take values of a different type. In my case Object, String and Int are allowed. The Object itself consists of 1 Int and 1 String.

The object I need to decode looks as:

I

{
   field1: 32,
   ...
   value: {
      id: 23,
      text: "text"
   }
}

II

{
   field1: 32,
   ...
   value: 21
}

III

{
   field1: 32,
   ...
   value: "value"
}

How to deal with such objects in circe?

like image 540
Some Name Avatar asked Dec 07 '22 10:12

Some Name


2 Answers

Let's say your case class would be:

@JsonCodec(decodeOnly = true)
case class X(id: Int, text: String)

Then I could assume that your field would be of type:

type Mixed = X Either Int Either String

decode for which could look like:

implicit val mixedDecoder: Decoder[Mixed] = 
  Decoder[X].map[Mixed](x => Left(Left(x))) or Decoder[Int].map[Mixed](i => Left(Right(i))) or Decoder[String].map[Mixed](s => Right(s))

You could derive codecs for Either if you define how they would be combined: left wins, right wins, or whatever you prefer:

implicit def eitherDecode[L: Decoder, R: Decoder]: Decoder[L Either R] =
  Decoder[L].map[L Either R](Left(_)) or Decoder[R].map[L Either R](Right(_))

Alternatively, you can create your own ADT (sealed trait + case classes) and then writing handwritten decoder to avoid using discriminator field.

The bottom line is that you have to somehow express that polymorphism in the type you are decoding into (in a sane manner - Any doesn't count) and then provide a decoder that would decode into it. And then you could simply use it:

@JsonCodec(decodeOnly = true)
case class BigClass(field1: String, value: Mixed)
like image 107
Mateusz Kubuszok Avatar answered Dec 27 '22 11:12

Mateusz Kubuszok


A similar approach to what @SomeName created, but with a decoder not requiring HCursor:

sealed trait Value
object Value {
  final case class Values(id: Int, text: String) extends Value
  final case class IntValue(i: Int) extends Value
  final case class StringValue(s: String) extends Value

  implicit val valueDecoder: Decoder[Value] = Decoder[String]
    .map[Value](StringValue)
    .or(Decoder[Int].map[Value](IntValue))
    .or(Decoder.forProduct2("id", "text")(Values.apply).map[Value](identity))
}

And the enclosing object:

final case class Example(field1: Int, value: Value)
object Example {
  implicit val exampDecoder: Decoder[Example] =
    Decoder.forProduct2("field1", "value")(Example.apply)
}

Run it:

import io.circe.Decoder
import io.circe.parser._

def main(args: Array[String]): Unit = {
  val fst =
    """
      |{
      |   "field1": 32,
      |   "value": {
      |      "id": 23,
      |      "text": "text"
      |   }
      |}""".stripMargin

  val snd =
    """
      |{
      |   "field1": 32,
      |   "value": 21
      |}
      |""".stripMargin

  val third =
    """{
      |   "field1": 32,
      |   "value": "value"
      |}
      |""".stripMargin

  println(decode[Example](fst))
  println(decode[Example](snd))
  println(decode[Example](third))
}

Results:

Right(Example(32,Values(23,text)))
Right(Example(32,IntValue(21)))
Right(Example(32,StringValue(value)))
like image 37
Yuval Itzchakov Avatar answered Dec 27 '22 11:12

Yuval Itzchakov