Here’s one way you can quickly decode a JSON using circe:
import io.circe._
import io.circe.generic.semiauto.deriveDecoder
import io.circe.literal._
case class Example(id: String, field1: String)
object Example {
implicit val circeDecoder: Decoder\[Example\] = deriveDecoder\[Example\]
}
val exampleJsonIdAsString: Json =
json"""{
"id": "12345",
"field1": "something"
}"""
val example = exampleJsonIdAsString.as\[Example\]
println(example)
This prints Right(Example(12345,something))
.
This all works well if the types of the JSON fields are predictable. But what if you’re consuming some API that you don’t control and the same field sometimes comes with different types? For example, an id
field coming sometimes as string and other times as number. (I had this happening to me recently)
val exampleJsonIdAsLong: Json =
json"""{
"id": 12345,
"field1": "something"
}"""
val example = exampleJsonIdAsLong.as\[Example\]
println(example)
This will fail with Left(DecodingFailure(String, List(DownField(id))))
since is is expecting id
to be a string
but it is a number
So, our decoder has to know to deal with the id
field being string or number. Now an automatic decoder will not do and we’ll have to write a custom one:
object Example {
implicit val circeDecoder: Decoder\[Example\] = (c: HCursor) => {
val idField = "id"
val idParsed: Result\[String\] = c.downField(idField).as\[String\] match {
case Right(v) => Right(v)
case Left(_) => c.downField(idField).as\[Int\] match {
case Right(v) => Right(v.toString)
case Left(err) => Left(err)
}
}
idParsed match {
case Right(v) =>
val transformedJson = c.withFocus(_.mapObject(_.add(idField, Json.fromString(v)))).success.get
deriveDecoder\[Example\].apply(transformedJson)
case Left(err) => Left(err)
}
}
}
This will successfully parse both “types” of JSON. But the decoder is a little cumbersome to read. And I would like to reuse this logic in other decoders. Here’s an alternate approach:
import io.circe._
import io.circe.generic.semiauto.deriveDecoder
import io.circe.literal._
import io.estatico.newtype.macros.newtype
@newtype case class MyId(private val value: String)
object MyId {
implicit val circeDecoder: Decoder\[MyId\] = (c: HCursor) => {
c.value.asString match {
case Some(v) => Right(MyId(v))
case None => c.value.asNumber match {
case Some(v) => Right(MyId(v.toString))
case None => Left(DecodingFailure(s"Can't decode: ${c.value}", List()))
}
}
}
}
case class Example(id: MyId, field1: String)
object Example {
implicit val circeDecoder: Decoder\[Example\] = deriveDecoder\[Example\]
}
We define a newtype value called MyId
, write a custom decoder for it and then go back to using an automatic decoder for Example
. The MyId
type can then be reused in other case classes.