CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-circe--circe-core

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.

Pending
Overview
Eval results
Files

key-encoding-decoding.mddocs/

Key Encoding and Decoding

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.

KeyEncoder[A]

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]
}

KeyEncoder Companion Object

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]
}

KeyDecoder[A]

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]
}

KeyDecoder Companion Object

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]
}

Usage Examples

Basic Key Encoding

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 representations

Basic Key Decoding

import 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 integers

Custom Key Encoders

import 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"}

Custom Key Decoders

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"))

Key Encoder Contramap

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"]}

Key Decoder Combinators

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]]

Working with Complex Keys

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"))

Error Handling with Key Decoders

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")
}

UUID and URI Key Support

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]]

Numeric Key Types

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]])

Syntax Extensions

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

docs

cursor-navigation.md

error-handling.md

index.md

json-data-types.md

json-printing.md

key-encoding-decoding.md

type-classes.md

tile.json