scalacircehttp4s-circe

Encoding and Decoding for Student Case Class with Animal Trait


Problem Description

I have a case class Student that includes instances of the Animal trait, along with two case classes Cat and Dog that extend the trait. I'm having trouble figuring out how to properly encode and decode instances of the Student class.

Goals Understand how to correctly encode a Student instance to JSON. Learn the appropriate decoding process for a JSON representation back to a Student instance.

import io.circe._
import io.circe.generic.semiauto._

trait Animal

case class Cat(name: String) extends Animal

case class Dog(name: String) extends Animal

case class Student(id: Int, name: String, animal: Animal)

object Student {

  implicit val catEncoder: Encoder[Cat] = deriveEncoder
  implicit val catDecoder: Decoder[Cat] = deriveDecoder
  implicit val dogEncoder: Encoder[Dog] = deriveEncoder
  implicit val dogDecoder: Decoder[Dog] = deriveDecoder

  implicit val animalEncoder: Encoder[Animal] = deriveEncoder
  implicit val animalDecoder: Decoder[Animal] = deriveDecoder

  implicit val studentEncoder: Encoder[Student] = deriveEncoder
  implicit val studentDecoder: Decoder[Student] = deriveDecoder
}

object Test extends App {

  import io.circe.syntax._

  val student = Student(1, "abc", Cat("cat"))

  val jsonString = student.asJson.noSpaces
  println("JSON representation: " + jsonString)

  val decoded: Either[io.circe.Error, Student] = io.circe.parser.decode[Student](jsonString)
  println("Decoded Student: " + decoded)
}


Solution

  • If you'd like to derive a type class (codec) for a parent trait of hierarchy then the trait must be sealed

    sealed trait Animal
    

    https://scastie.scala-lang.org/DmytroMitin/td1vmuCESZePhxno7VYwUw/4

    //JSON representation: {"id":1,"name":"abc","animal":{"Cat":{"name":"cat"}}}
    //Decoded Student: Right(Student(1,abc,Cat(cat)))
    

    Also on contrary to Student, for Cat, Dog, Animal the instances of type classes Encoder/Decoder are placed now out of implicit scope.

    Where does Scala look for implicits?

    Implicits of types SomeTypeclass[SomeDatatype] should be placed to companion objects of SomeTypeclass or SomeDatatype. Then the implicits will be available without import. You can check that although implicitly[Decoder[Student]] compiles but for example implicitly[Decoder[Cat]] doesn't. So you should better write

    sealed trait Animal
    object Animal {
      implicit val animalEncoder: Encoder[Animal] = deriveEncoder
      implicit val animalDecoder: Decoder[Animal] = deriveDecoder
    }
    
    case class Cat(name: String) extends Animal
    object Cat {
      implicit val catEncoder: Encoder[Cat] = deriveEncoder
      implicit val catDecoder: Decoder[Cat] = deriveDecoder
    }
    
    case class Dog(name: String) extends Animal
    object Dog {
      implicit val dogEncoder: Encoder[Dog] = deriveEncoder
      implicit val dogDecoder: Decoder[Dog] = deriveDecoder
    }
    
    case class Student(id: Int, name: String, animal: Animal)
    object Student {
      implicit val studentEncoder: Encoder[Student] = deriveEncoder
      implicit val studentDecoder: Decoder[Student] = deriveDecoder
    }
    

    https://scastie.scala-lang.org/DmytroMitin/td1vmuCESZePhxno7VYwUw/2

    Also Encoder + Decoder is just Codec

    sealed trait Animal
    object Animal {
      implicit val animalCodec: Codec[Animal] = deriveCodec
    }
    
    case class Cat(name: String) extends Animal
    object Cat {
      implicit val catCodec: Codec[Cat] = deriveCodec
    }
    
    case class Dog(name: String) extends Animal
    object Dog {
      implicit val dogCodec: Codec[Dog] = deriveCodec
    }
    
    case class Student(id: Int, name: String, animal: Animal)
    object Student {
      implicit val studentCodec: Codec[Student] = deriveCodec
    }
    

    https://scastie.scala-lang.org/DmytroMitin/td1vmuCESZePhxno7VYwUw/5