Core module of circe, a JSON library for Scala that enables developers to encode and decode JSON data with type safety and functional programming principles.
—
Circe provides type-safe encoding and decoding of JSON object keys through the KeyEncoder and KeyDecoder type classes. This enables working with Maps where keys are not just strings.
Converts values of type A to string keys for JSON objects.
trait KeyEncoder[A] extends Serializable {
// Core method
def apply(key: A): String
// Combinators
def contramap[B](f: B => A): KeyEncoder[B]
}object KeyEncoder {
// Utilities
def apply[A](implicit A: KeyEncoder[A]): KeyEncoder[A]
def instance[A](f: A => String): KeyEncoder[A]
// Primitive instances
implicit val encodeKeyString: KeyEncoder[String]
implicit val encodeKeySymbol: KeyEncoder[Symbol]
implicit val encodeKeyByte: KeyEncoder[Byte]
implicit val encodeKeyShort: KeyEncoder[Short]
implicit val encodeKeyInt: KeyEncoder[Int]
implicit val encodeKeyLong: KeyEncoder[Long]
implicit val encodeKeyDouble: KeyEncoder[Double]
// Utility type instances
implicit val encodeKeyUUID: KeyEncoder[UUID]
implicit val encodeKeyURI: KeyEncoder[URI]
// Type class instance
implicit val keyEncoderContravariant: Contravariant[KeyEncoder]
}Converts string keys from JSON objects to values of type A.
trait KeyDecoder[A] extends Serializable {
// Core method
def apply(key: String): Option[A]
// Combinators
def map[B](f: A => B): KeyDecoder[B]
def flatMap[B](f: A => KeyDecoder[B]): KeyDecoder[B]
}object KeyDecoder {
// Utilities
def apply[A](implicit A: KeyDecoder[A]): KeyDecoder[A]
def instance[A](f: String => Option[A]): KeyDecoder[A]
// Always-successful decoder base class
abstract class AlwaysKeyDecoder[A] extends KeyDecoder[A] {
def decodeSafe(key: String): A
final def apply(key: String): Option[A] = Some(decodeSafe(key))
}
// Primitive instances
implicit val decodeKeyString: KeyDecoder[String]
implicit val decodeKeySymbol: KeyDecoder[Symbol]
implicit val decodeKeyByte: KeyDecoder[Byte]
implicit val decodeKeyShort: KeyDecoder[Short]
implicit val decodeKeyInt: KeyDecoder[Int]
implicit val decodeKeyLong: KeyDecoder[Long]
implicit val decodeKeyDouble: KeyDecoder[Double]
// Utility type instances
implicit val decodeKeyUUID: KeyDecoder[UUID]
implicit val decodeKeyURI: KeyDecoder[URI]
// Type class instance
implicit val keyDecoderInstances: MonadError[KeyDecoder, Unit]
}import io.circe._
import io.circe.syntax._
// String keys (identity)
val stringMap = Map("name" -> "John", "age" -> "30")
val stringJson = stringMap.asJson
// Result: {"name": "John", "age": "30"}
// Integer keys
val intMap = Map(1 -> "first", 2 -> "second", 3 -> "third")
val intJson = intMap.asJson
// Result: {"1": "first", "2": "second", "3": "third"}
// UUID keys
import java.util.UUID
val uuid1 = UUID.fromString("550e8400-e29b-41d4-a716-446655440000")
val uuid2 = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
val uuidMap = Map(uuid1 -> "first", uuid2 -> "second")
val uuidJson = uuidMap.asJson
// Keys become UUID string representationsimport io.circe._
import io.circe.parser._
import scala.collection.immutable.Map
// Decode to string keys
val json = parse("""{"name": "John", "age": "30"}""").getOrElse(Json.Null)
val stringResult: Either[DecodingFailure, Map[String, String]] = json.as[Map[String, String]]
// Result: Right(Map("name" -> "John", "age" -> "30"))
// Decode to integer keys
val intJson = parse("""{"1": "first", "2": "second", "3": "third"}""").getOrElse(Json.Null)
val intResult: Either[DecodingFailure, Map[Int, String]] = intJson.as[Map[Int, String]]
// Result: Right(Map(1 -> "first", 2 -> "second", 3 -> "third"))
// Failed key decoding
val invalidIntJson = parse("""{"abc": "first", "def": "second"}""").getOrElse(Json.Null)
val failedResult: Either[DecodingFailure, Map[Int, String]] = invalidIntJson.as[Map[Int, String]]
// Result: Left(DecodingFailure(...)) - "abc" and "def" can't be parsed as integersimport io.circe._
case class UserId(id: Long)
// Custom key encoder for UserId
implicit val userIdKeyEncoder: KeyEncoder[UserId] = KeyEncoder.instance(_.id.toString)
// Usage with maps
val userMap = Map(
UserId(123) -> "Alice",
UserId(456) -> "Bob"
)
val json = userMap.asJson
// Result: {"123": "Alice", "456": "Bob"}import io.circe._
case class UserId(id: Long)
// Custom key decoder for UserId
implicit val userIdKeyDecoder: KeyDecoder[UserId] = KeyDecoder.instance { str =>
try {
Some(UserId(str.toLong))
} catch {
case _: NumberFormatException => None
}
}
// Usage
import io.circe.parser._
val json = parse("""{"123": "Alice", "456": "Bob"}""").getOrElse(Json.Null)
val result: Either[DecodingFailure, Map[UserId, String]] = json.as[Map[UserId, String]]
// Result: Right(Map(UserId(123) -> "Alice", UserId(456) -> "Bob"))import io.circe._
case class ProductId(value: String)
case class CategoryId(value: String)
// Create key encoders using contramap
implicit val productIdKeyEncoder: KeyEncoder[ProductId] =
KeyEncoder[String].contramap(_.value)
implicit val categoryIdKeyEncoder: KeyEncoder[CategoryId] =
KeyEncoder[String].contramap(_.value)
// Usage
val productMap = Map(
ProductId("laptop") -> 999.99,
ProductId("mouse") -> 29.99
)
val categoryMap = Map(
CategoryId("electronics") -> List("laptop", "mouse"),
CategoryId("books") -> List("fiction", "non-fiction")
)
val productJson = productMap.asJson
// Result: {"laptop": 999.99, "mouse": 29.99}
val categoryJson = categoryMap.asJson
// Result: {"electronics": ["laptop", "mouse"], "books": ["fiction", "non-fiction"]}import io.circe._
case class UserId(id: Long)
// Using map combinator
implicit val userIdKeyDecoder: KeyDecoder[UserId] =
KeyDecoder[Long].map(UserId.apply)
// Using flatMap for validation
case class ValidatedId(id: Long)
implicit val validatedIdKeyDecoder: KeyDecoder[ValidatedId] =
KeyDecoder[Long].flatMap { id =>
if (id > 0) KeyDecoder.instance(_ => Some(ValidatedId(id)))
else KeyDecoder.instance(_ => None)
}
// Usage
import io.circe.parser._
val json = parse("""{"123": "valid", "-1": "invalid"}""").getOrElse(Json.Null)
// This will succeed for key "123" but fail for key "-1"
val result = json.as[Map[ValidatedId, String]]import io.circe._
import java.time.LocalDate
import java.time.format.DateTimeFormatter
case class DateKey(date: LocalDate)
// Custom key encoder/decoder for dates
implicit val dateKeyEncoder: KeyEncoder[DateKey] = KeyEncoder.instance { dateKey =>
dateKey.date.format(DateTimeFormatter.ISO_LOCAL_DATE)
}
implicit val dateKeyDecoder: KeyDecoder[DateKey] = KeyDecoder.instance { str =>
try {
Some(DateKey(LocalDate.parse(str, DateTimeFormatter.ISO_LOCAL_DATE)))
} catch {
case _: Exception => None
}
}
// Usage
val dateMap = Map(
DateKey(LocalDate.of(2023, 1, 1)) -> "New Year",
DateKey(LocalDate.of(2023, 12, 25)) -> "Christmas"
)
val json = dateMap.asJson
// Result: {"2023-01-01": "New Year", "2023-12-25": "Christmas"}
// Decode back
import io.circe.parser._
val parsedJson = parse(json.noSpaces).getOrElse(Json.Null)
val decodedMap = parsedJson.as[Map[DateKey, String]]
// Result: Right(Map(DateKey(2023-01-01) -> "New Year", DateKey(2023-12-25) -> "Christmas"))import io.circe._
import io.circe.parser._
// Key decoder that can fail
implicit val strictIntKeyDecoder: KeyDecoder[Int] = KeyDecoder.instance { str =>
if (str.forall(_.isDigit)) {
try Some(str.toInt)
catch { case _: NumberFormatException => None }
} else None
}
val mixedJson = parse("""{"123": "valid", "abc": "invalid", "456": "also valid"}""").getOrElse(Json.Null)
val result = mixedJson.as[Map[Int, String]]
// This will fail because "abc" cannot be decoded as an Int
result match {
case Left(decodingFailure) =>
println(s"Failed to decode keys: ${decodingFailure.message}")
case Right(map) =>
println(s"Successfully decoded: $map")
}import io.circe._
import io.circe.syntax._
import java.util.UUID
import java.net.URI
// UUID keys
val uuidMap = Map(
UUID.randomUUID() -> "first",
UUID.randomUUID() -> "second"
)
val uuidJson = uuidMap.asJson
// Keys become UUID string representations
// URI keys
val uriMap = Map(
new URI("http://example.com") -> "website",
new URI("ftp://files.example.com") -> "file server"
)
val uriJson = uriMap.asJson
// Keys become URI string representations
// Decoding back
import io.circe.parser._
val parsedUuidJson = parse(uuidJson.noSpaces).getOrElse(Json.Null)
val decodedUuidMap = parsedUuidJson.as[Map[UUID, String]]
val parsedUriJson = parse(uriJson.noSpaces).getOrElse(Json.Null)
val decodedUriMap = parsedUriJson.as[Map[URI, String]]import io.circe._
import io.circe.syntax._
// All numeric types are supported as keys
val byteMap = Map[Byte, String](1.toByte -> "one", 2.toByte -> "two")
val shortMap = Map[Short, String](10.toShort -> "ten", 20.toShort -> "twenty")
val longMap = Map[Long, String](1000L -> "thousand", 2000L -> "two thousand")
val doubleMap = Map[Double, String](1.5 -> "one and half", 2.7 -> "two point seven")
// All encode to string keys
val byteJson = byteMap.asJson // {"1": "one", "2": "two"}
val shortJson = shortMap.asJson // {"10": "ten", "20": "twenty"}
val longJson = longMap.asJson // {"1000": "thousand", "2000": "two thousand"}
val doubleJson = doubleMap.asJson // {"1.5": "one and half", "2.7": "two point seven"}
// And can be decoded back
import io.circe.parser._
val decodedByteMap = parse(byteJson.noSpaces).flatMap(_.as[Map[Byte, String]])
val decodedShortMap = parse(shortJson.noSpaces).flatMap(_.as[Map[Short, String]])
val decodedLongMap = parse(longJson.noSpaces).flatMap(_.as[Map[Long, String]])
val decodedDoubleMap = parse(doubleJson.noSpaces).flatMap(_.as[Map[Double, String]])import io.circe._
import io.circe.syntax._
case class ProductId(id: String)
implicit val productIdKeyEncoder: KeyEncoder[ProductId] = KeyEncoder[String].contramap(_.id)
val productId = ProductId("laptop-123")
val price = 999.99
// Using the := operator for key-value pairs
val keyValuePair: (String, Json) = productId := price
// Result: ("laptop-123", Json number 999.99)
// This is equivalent to:
val manualPair = (productIdKeyEncoder(productId), price.asJson)Install with Tessl CLI
npx tessl i tessl/maven-io-circe--circe-core