Custom encoders/decoders

If you want to write your own codec instead of using automatic or semi-automatic derivation, you can do so in a couple of ways.

Firstly, you can write a new Encoder[A] and Decoder[A] from scratch:

import io.circe.{ Decoder, Encoder, HCursor, Json }
// import io.circe.{Decoder, Encoder, HCursor, Json}

class Thing(val foo: String, val bar: Int)
// defined class Thing

implicit val encodeFoo: Encoder[Thing] = new Encoder[Thing] {
  final def apply(a: Thing): Json = Json.obj(
    ("foo", Json.fromString(a.foo)),
    ("bar", Json.fromInt(a.bar))
  )
}
// encodeFoo: io.circe.Encoder[Thing] = $anon$1@50acf339

implicit val decodeFoo: Decoder[Thing] = new Decoder[Thing] {
  final def apply(c: HCursor): Decoder.Result[Thing] =
    for {
      foo <- c.downField("foo").as[String]
      bar <- c.downField("bar").as[Int]
    } yield {
      new Thing(foo, bar)
    }
}
// decodeFoo: io.circe.Decoder[Thing] = $anon$1@1305ac0

But in many cases you might find it more convenient to piggyback on top of the decoders that are already available. For example, a codec for java.time.Instant might look like this:

import cats.syntax.either._
// import cats.syntax.either._

import io.circe.{ Decoder, Encoder }
// import io.circe.{Decoder, Encoder}

import java.time.Instant
// import java.time.Instant

implicit val encodeInstant: Encoder[Instant] = Encoder.encodeString.contramap[Instant](_.toString)
// encodeInstant: io.circe.Encoder[java.time.Instant] = io.circe.Encoder$$anon$1@1f30c0bf

implicit val decodeInstant: Decoder[Instant] = Decoder.decodeString.emap { str =>
  Either.catchNonFatal(Instant.parse(str)).leftMap(t => "Instant")
}
// decodeInstant: io.circe.Decoder[java.time.Instant] = io.circe.Decoder$$anon$12@63ac8612

Custom key types

If you need to encode/decode Map[K, V] where K is not String (or Symbol, Int, Long, etc.), you need to provide a KeyEncoder and/or KeyDecoder for your custom key type.

For example:

import io.circe._, io.circe.syntax._
// import io.circe._
// import io.circe.syntax._

case class Foo(value: String)
// defined class Foo

implicit val fooKeyEncoder: KeyEncoder[Foo] = new KeyEncoder[Foo] {
  override def apply(foo: Foo): String = foo.value
}
// fooKeyEncoder: io.circe.KeyEncoder[Foo] = $anon$1@768b2f94

val map = Map[Foo, Int](
  Foo("hello") -> 123,
  Foo("world") -> 456
)
// map: scala.collection.immutable.Map[Foo,Int] = Map(Foo(hello) -> 123, Foo(world) -> 456)

val json = map.asJson
// json: io.circe.Json =
// {
//   "hello" : 123,
//   "world" : 456
// }

implicit val fooKeyDecoder: KeyDecoder[Foo] = new KeyDecoder[Foo] {
  override def apply(key: String): Option[Foo] = Some(Foo(key))
}
// fooKeyDecoder: io.circe.KeyDecoder[Foo] = $anon$1@5ee6edc

json.as[Map[Foo, Int]]
// res0: io.circe.Decoder.Result[Map[Foo,Int]] = Right(Map(Foo(hello) -> 123, Foo(world) -> 456))

Custom key mappings via annotations

It’s often necessary to work with keys in your JSON objects that aren’t idiomatic case class member names in Scala. While the standard generic derivation doesn’t support this use case, the experimental circe-generic-extras module does provide two ways to transform your case class member names during encoding and decoding.

In many cases the transformation is as simple as going from camel case to snake case, in which case all you need is a custom implicit configuration:

import io.circe.generic.extras._, io.circe.syntax._
// import io.circe.generic.extras._
// import io.circe.syntax._

implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
// config: io.circe.generic.extras.Configuration = Configuration(io.circe.generic.extras.Configuration$$$Lambda$8574/1603322952@39f4e6bd,io.circe.generic.extras.Configuration$$$Lambda$8573/458102819@52106ea0,false,None)

@ConfiguredJsonCodec case class User(firstName: String, lastName: String)
// defined class User
// defined object User

User("Foo", "McBar").asJson
// res1: io.circe.Json =
// {
//   "first_name" : "Foo",
//   "last_name" : "McBar"
// }

In other cases you may need more complex mappings. These can be provided as a function:

import io.circe.generic.extras._, io.circe.syntax._
// import io.circe.generic.extras._
// import io.circe.syntax._

implicit val config: Configuration = Configuration.default.copy(
  transformMemberNames = {
    case "i" => "my-int"
    case other => other
  }
)
// config: io.circe.generic.extras.Configuration = Configuration($$Lambda$8846/1397772732@a2dbe33,io.circe.generic.extras.Configuration$$$Lambda$8573/458102819@52106ea0,false,None)

@ConfiguredJsonCodec case class Bar(i: Int, s: String)
// defined class Bar
// defined object Bar

Bar(13, "Qux").asJson
// res2: io.circe.Json =
// {
//   "my-int" : 13,
//   "s" : "Qux"
// }

Since this is a common use case, we also support for mapping member names via an annotation:

import io.circe.generic.extras._, io.circe.syntax._
// import io.circe.generic.extras._
// import io.circe.syntax._

implicit val config: Configuration = Configuration.default
// config: io.circe.generic.extras.Configuration = Configuration(io.circe.generic.extras.Configuration$$$Lambda$8572/668885810@4b622e28,io.circe.generic.extras.Configuration$$$Lambda$8573/458102819@52106ea0,false,None)

@ConfiguredJsonCodec case class Bar(@JsonKey("my-int") i: Int, s: String)
// defined class Bar
// defined object Bar

Bar(13, "Qux").asJson
// res3: io.circe.Json =
// {
//   "my-int" : 13,
//   "s" : "Qux"
// }

It’s worth noting that if you don’t want to use the experimental generic-extras module, the completely unmagical forProductN version isn’t really that much of a burden:

import io.circe.Encoder, io.circe.syntax._
// import io.circe.Encoder
// import io.circe.syntax._

case class User(firstName: String, lastName: String)
// defined class User

case class Bar(i: Int, s: String)
// defined class Bar

implicit val encodeUser: Encoder[User] =
  Encoder.forProduct2("first_name", "last_name")(u => (u.firstName, u.lastName))
// encodeUser: io.circe.Encoder[User] = io.circe.ProductEncoders$$anon$2@38169c6

implicit val encodeBar: Encoder[Bar] =
  Encoder.forProduct2("my-int", "s")(b => (b.i, b.s))
// encodeBar: io.circe.Encoder[Bar] = io.circe.ProductEncoders$$anon$2@1e8f7db0

User("Foo", "McBar").asJson
// res4: io.circe.Json =
// {
//   "first_name" : "Foo",
//   "last_name" : "McBar"
// }

Bar(13, "Qux").asJson
// res5: io.circe.Json =
// {
//   "my-int" : 13,
//   "s" : "Qux"
// }

While this version does involve a bit of boilerplate, it only requires circe-core, and may have slightly better runtime performance in some cases.