Search code examples
jsonscalacirce

Encode nested object as a string


Given the following case classes:

case class Mailbox(value: String)
case class Group(objectType: String, mailbox: Mailbox)

I am attempting to find a way to encode a Group object as follows where mailbox is encoded as a string value, rather than an object:

{
  "objectType" : "Group",
  "mailbox" : "mailto:[email protected]"
}

With automatic derivation, encoding/decoding both succeed but I end up with a result like the following as would be expected:

{
  "objectType" : "Group",
  "mailbox" : {
    "value" : "mailto:[email protected]"
  }
}

I can achieve the result that I want by adding a custom encoder like the following:

object Mailbox {
  implicit val encoder: Encoder[Mailbox] = (m: Mailbox) => Json.fromString(m.value)
  implicit val decoder: Decoder[Mailbox] = deriveDecoder[Mailbox]
}

However, then decoding fails with the following:

DecodingFailure(Attempt to decode value on failed cursor, List(DownField(value), DownField(mailbox)))

I've attempted to resolve this by also writing a custom decoder for Mailbox but get the same result. Any guidance on the correct way to approach this situation would be appreciated.

Here is the complete code:

case class Mailbox(value: String)
object Mailbox {
  implicit val encoder: Encoder[Mailbox] = (m: Mailbox) => Json.fromString(m.value)
  implicit val decoder: Decoder[Mailbox] = deriveDecoder[Mailbox]
}

case class Group(objectType: String, mailbox: Mailbox)

object Sandbox {
  def main(args: Array[String]): Unit = {

    val group: Group = Group("Group", Mailbox("mailto:[email protected]"))
    val json: String = group.asJson.spaces2
    println(json)

    parser.decode[Group](json) match {
      case Right(group) => println(group)
      case Left(err) => println(err)
    }
  }
}

Note that this is a derived example, meant only to demonstrate my question.


Solution

  • You can use map / contramap instead to map your type to String and back:

    object Mailbox {
    
      implicit val encoder: Encoder[Mailbox] = Encoder.encodeString.contramap[Mailbox](_.value)
      implicit val decoder: Decoder[Mailbox] = Decoder.decodeString.map[Mailbox](Mailbox.apply)
    }
    
    

    A documentation describing almost exactly this scenario is at Custom encoders/decoders