Skip to content

Instantly share code, notes, and snippets.

@filosganga
Last active October 27, 2021 06:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save filosganga/0ea61a4b7859a42c77feb4fa6e08c6bf to your computer and use it in GitHub Desktop.
Save filosganga/0ea61a4b7859a42c77feb4fa6e08c6bf to your computer and use it in GitHub Desktop.
The JSON tagger allows to decode an ADT encoded in JSON avoiding the focus to switch to a different branch
import io.circe._
import io.circe.generic.semiauto._
import cats.syntax.all._
object JsonTagger {
private implicit class Tagger[A](d: Decoder[A]) {
def tag(accessor: String): Decoder[Decoder[A]] =
Decoder
.instance { inputJson =>
inputJson.downField(accessor) match {
case err: FailedCursor => Left(DecodingFailure(s"key $accessor expected", err.history))
case innerJson: HCursor if innerJson.value.isNull =>
Left(DecodingFailure(s"key $accessor expected to be not null", innerJson.history))
case innerJson: HCursor => Right(innerJson)
}
}
.map(outJson => Decoder.instance(_ => d(outJson)))
}
sealed trait Animal
object Animal {
case class Dog(name: String, tailLength: Int) extends Animal
case class Cat(name: String, preferredMilk: String) extends Animal
}
/**
* This decoder does not return an useful message, because it will always return the failure from the `decodeDog` even if the JSON is for a cat.
*
* {{{
* {
* "animal": {
* "cat": {
* "name": "Toby"
* }
* }
* }
* }}}
*
* This JSON snippet will fail for something like `dog` is an ivalid cursor.
*/
val buggedDecoderForAnimal: Decoder[Animal] = {
val decodeCat = deriveDecoder[Cat].prepare(hc => hc.downField("cat"))
val decodeDog = deriveDecoder[Dog].prepare(hc => hc.downField("dog"))
decodeCat.orElse(decodeDog)
}
/**
* This decoder returns an useful message, because it will not fallback to the other decoder if "cat" or "dog" keys exist.
*
* {{{
* {
* "animal": {
* "cat": {
* "name": "Toby"
* }
* }
* }
* }}}
*
* This JSON snippet will fail for something like `preferredMilk` is null or an ivalid cursor. So it does not fallback to "dog"
. */
val taggedDecoderForAnimal: Decoder[Animal] = {
val decodeCat = deriveDecoder[Cat].tag("cat")
val decodeDog = deriveDecoder[Dog].tag("dog")
decodeCat.orElse(decodeDog)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment