0
# Key Encoding and Decoding
1
2
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.
3
4
## KeyEncoder[A]
5
6
Converts values of type `A` to string keys for JSON objects.
7
8
```scala { .api }
9
trait KeyEncoder[A] extends Serializable {
10
// Core method
11
def apply(key: A): String
12
13
// Combinators
14
def contramap[B](f: B => A): KeyEncoder[B]
15
}
16
```
17
18
### KeyEncoder Companion Object
19
20
```scala { .api }
21
object KeyEncoder {
22
// Utilities
23
def apply[A](implicit A: KeyEncoder[A]): KeyEncoder[A]
24
def instance[A](f: A => String): KeyEncoder[A]
25
26
// Primitive instances
27
implicit val encodeKeyString: KeyEncoder[String]
28
implicit val encodeKeySymbol: KeyEncoder[Symbol]
29
implicit val encodeKeyByte: KeyEncoder[Byte]
30
implicit val encodeKeyShort: KeyEncoder[Short]
31
implicit val encodeKeyInt: KeyEncoder[Int]
32
implicit val encodeKeyLong: KeyEncoder[Long]
33
implicit val encodeKeyDouble: KeyEncoder[Double]
34
35
// Utility type instances
36
implicit val encodeKeyUUID: KeyEncoder[UUID]
37
implicit val encodeKeyURI: KeyEncoder[URI]
38
39
// Type class instance
40
implicit val keyEncoderContravariant: Contravariant[KeyEncoder]
41
}
42
```
43
44
## KeyDecoder[A]
45
46
Converts string keys from JSON objects to values of type `A`.
47
48
```scala { .api }
49
trait KeyDecoder[A] extends Serializable {
50
// Core method
51
def apply(key: String): Option[A]
52
53
// Combinators
54
def map[B](f: A => B): KeyDecoder[B]
55
def flatMap[B](f: A => KeyDecoder[B]): KeyDecoder[B]
56
}
57
```
58
59
### KeyDecoder Companion Object
60
61
```scala { .api }
62
object KeyDecoder {
63
// Utilities
64
def apply[A](implicit A: KeyDecoder[A]): KeyDecoder[A]
65
def instance[A](f: String => Option[A]): KeyDecoder[A]
66
67
// Always-successful decoder base class
68
abstract class AlwaysKeyDecoder[A] extends KeyDecoder[A] {
69
def decodeSafe(key: String): A
70
final def apply(key: String): Option[A] = Some(decodeSafe(key))
71
}
72
73
// Primitive instances
74
implicit val decodeKeyString: KeyDecoder[String]
75
implicit val decodeKeySymbol: KeyDecoder[Symbol]
76
implicit val decodeKeyByte: KeyDecoder[Byte]
77
implicit val decodeKeyShort: KeyDecoder[Short]
78
implicit val decodeKeyInt: KeyDecoder[Int]
79
implicit val decodeKeyLong: KeyDecoder[Long]
80
implicit val decodeKeyDouble: KeyDecoder[Double]
81
82
// Utility type instances
83
implicit val decodeKeyUUID: KeyDecoder[UUID]
84
implicit val decodeKeyURI: KeyDecoder[URI]
85
86
// Type class instance
87
implicit val keyDecoderInstances: MonadError[KeyDecoder, Unit]
88
}
89
```
90
91
## Usage Examples
92
93
### Basic Key Encoding
94
95
```scala
96
import io.circe._
97
import io.circe.syntax._
98
99
// String keys (identity)
100
val stringMap = Map("name" -> "John", "age" -> "30")
101
val stringJson = stringMap.asJson
102
// Result: {"name": "John", "age": "30"}
103
104
// Integer keys
105
val intMap = Map(1 -> "first", 2 -> "second", 3 -> "third")
106
val intJson = intMap.asJson
107
// Result: {"1": "first", "2": "second", "3": "third"}
108
109
// UUID keys
110
import java.util.UUID
111
val uuid1 = UUID.fromString("550e8400-e29b-41d4-a716-446655440000")
112
val uuid2 = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
113
val uuidMap = Map(uuid1 -> "first", uuid2 -> "second")
114
val uuidJson = uuidMap.asJson
115
// Keys become UUID string representations
116
```
117
118
### Basic Key Decoding
119
120
```scala
121
import io.circe._
122
import io.circe.parser._
123
import scala.collection.immutable.Map
124
125
// Decode to string keys
126
val json = parse("""{"name": "John", "age": "30"}""").getOrElse(Json.Null)
127
val stringResult: Either[DecodingFailure, Map[String, String]] = json.as[Map[String, String]]
128
// Result: Right(Map("name" -> "John", "age" -> "30"))
129
130
// Decode to integer keys
131
val intJson = parse("""{"1": "first", "2": "second", "3": "third"}""").getOrElse(Json.Null)
132
val intResult: Either[DecodingFailure, Map[Int, String]] = intJson.as[Map[Int, String]]
133
// Result: Right(Map(1 -> "first", 2 -> "second", 3 -> "third"))
134
135
// Failed key decoding
136
val invalidIntJson = parse("""{"abc": "first", "def": "second"}""").getOrElse(Json.Null)
137
val failedResult: Either[DecodingFailure, Map[Int, String]] = invalidIntJson.as[Map[Int, String]]
138
// Result: Left(DecodingFailure(...)) - "abc" and "def" can't be parsed as integers
139
```
140
141
### Custom Key Encoders
142
143
```scala
144
import io.circe._
145
146
case class UserId(id: Long)
147
148
// Custom key encoder for UserId
149
implicit val userIdKeyEncoder: KeyEncoder[UserId] = KeyEncoder.instance(_.id.toString)
150
151
// Usage with maps
152
val userMap = Map(
153
UserId(123) -> "Alice",
154
UserId(456) -> "Bob"
155
)
156
157
val json = userMap.asJson
158
// Result: {"123": "Alice", "456": "Bob"}
159
```
160
161
### Custom Key Decoders
162
163
```scala
164
import io.circe._
165
166
case class UserId(id: Long)
167
168
// Custom key decoder for UserId
169
implicit val userIdKeyDecoder: KeyDecoder[UserId] = KeyDecoder.instance { str =>
170
try {
171
Some(UserId(str.toLong))
172
} catch {
173
case _: NumberFormatException => None
174
}
175
}
176
177
// Usage
178
import io.circe.parser._
179
val json = parse("""{"123": "Alice", "456": "Bob"}""").getOrElse(Json.Null)
180
val result: Either[DecodingFailure, Map[UserId, String]] = json.as[Map[UserId, String]]
181
// Result: Right(Map(UserId(123) -> "Alice", UserId(456) -> "Bob"))
182
```
183
184
### Key Encoder Contramap
185
186
```scala
187
import io.circe._
188
189
case class ProductId(value: String)
190
case class CategoryId(value: String)
191
192
// Create key encoders using contramap
193
implicit val productIdKeyEncoder: KeyEncoder[ProductId] =
194
KeyEncoder[String].contramap(_.value)
195
196
implicit val categoryIdKeyEncoder: KeyEncoder[CategoryId] =
197
KeyEncoder[String].contramap(_.value)
198
199
// Usage
200
val productMap = Map(
201
ProductId("laptop") -> 999.99,
202
ProductId("mouse") -> 29.99
203
)
204
205
val categoryMap = Map(
206
CategoryId("electronics") -> List("laptop", "mouse"),
207
CategoryId("books") -> List("fiction", "non-fiction")
208
)
209
210
val productJson = productMap.asJson
211
// Result: {"laptop": 999.99, "mouse": 29.99}
212
213
val categoryJson = categoryMap.asJson
214
// Result: {"electronics": ["laptop", "mouse"], "books": ["fiction", "non-fiction"]}
215
```
216
217
### Key Decoder Combinators
218
219
```scala
220
import io.circe._
221
222
case class UserId(id: Long)
223
224
// Using map combinator
225
implicit val userIdKeyDecoder: KeyDecoder[UserId] =
226
KeyDecoder[Long].map(UserId.apply)
227
228
// Using flatMap for validation
229
case class ValidatedId(id: Long)
230
231
implicit val validatedIdKeyDecoder: KeyDecoder[ValidatedId] =
232
KeyDecoder[Long].flatMap { id =>
233
if (id > 0) KeyDecoder.instance(_ => Some(ValidatedId(id)))
234
else KeyDecoder.instance(_ => None)
235
}
236
237
// Usage
238
import io.circe.parser._
239
val json = parse("""{"123": "valid", "-1": "invalid"}""").getOrElse(Json.Null)
240
241
// This will succeed for key "123" but fail for key "-1"
242
val result = json.as[Map[ValidatedId, String]]
243
```
244
245
### Working with Complex Keys
246
247
```scala
248
import io.circe._
249
import java.time.LocalDate
250
import java.time.format.DateTimeFormatter
251
252
case class DateKey(date: LocalDate)
253
254
// Custom key encoder/decoder for dates
255
implicit val dateKeyEncoder: KeyEncoder[DateKey] = KeyEncoder.instance { dateKey =>
256
dateKey.date.format(DateTimeFormatter.ISO_LOCAL_DATE)
257
}
258
259
implicit val dateKeyDecoder: KeyDecoder[DateKey] = KeyDecoder.instance { str =>
260
try {
261
Some(DateKey(LocalDate.parse(str, DateTimeFormatter.ISO_LOCAL_DATE)))
262
} catch {
263
case _: Exception => None
264
}
265
}
266
267
// Usage
268
val dateMap = Map(
269
DateKey(LocalDate.of(2023, 1, 1)) -> "New Year",
270
DateKey(LocalDate.of(2023, 12, 25)) -> "Christmas"
271
)
272
273
val json = dateMap.asJson
274
// Result: {"2023-01-01": "New Year", "2023-12-25": "Christmas"}
275
276
// Decode back
277
import io.circe.parser._
278
val parsedJson = parse(json.noSpaces).getOrElse(Json.Null)
279
val decodedMap = parsedJson.as[Map[DateKey, String]]
280
// Result: Right(Map(DateKey(2023-01-01) -> "New Year", DateKey(2023-12-25) -> "Christmas"))
281
```
282
283
### Error Handling with Key Decoders
284
285
```scala
286
import io.circe._
287
import io.circe.parser._
288
289
// Key decoder that can fail
290
implicit val strictIntKeyDecoder: KeyDecoder[Int] = KeyDecoder.instance { str =>
291
if (str.forall(_.isDigit)) {
292
try Some(str.toInt)
293
catch { case _: NumberFormatException => None }
294
} else None
295
}
296
297
val mixedJson = parse("""{"123": "valid", "abc": "invalid", "456": "also valid"}""").getOrElse(Json.Null)
298
299
val result = mixedJson.as[Map[Int, String]]
300
// This will fail because "abc" cannot be decoded as an Int
301
302
result match {
303
case Left(decodingFailure) =>
304
println(s"Failed to decode keys: ${decodingFailure.message}")
305
case Right(map) =>
306
println(s"Successfully decoded: $map")
307
}
308
```
309
310
### UUID and URI Key Support
311
312
```scala
313
import io.circe._
314
import io.circe.syntax._
315
import java.util.UUID
316
import java.net.URI
317
318
// UUID keys
319
val uuidMap = Map(
320
UUID.randomUUID() -> "first",
321
UUID.randomUUID() -> "second"
322
)
323
val uuidJson = uuidMap.asJson
324
// Keys become UUID string representations
325
326
// URI keys
327
val uriMap = Map(
328
new URI("http://example.com") -> "website",
329
new URI("ftp://files.example.com") -> "file server"
330
)
331
val uriJson = uriMap.asJson
332
// Keys become URI string representations
333
334
// Decoding back
335
import io.circe.parser._
336
val parsedUuidJson = parse(uuidJson.noSpaces).getOrElse(Json.Null)
337
val decodedUuidMap = parsedUuidJson.as[Map[UUID, String]]
338
339
val parsedUriJson = parse(uriJson.noSpaces).getOrElse(Json.Null)
340
val decodedUriMap = parsedUriJson.as[Map[URI, String]]
341
```
342
343
### Numeric Key Types
344
345
```scala
346
import io.circe._
347
import io.circe.syntax._
348
349
// All numeric types are supported as keys
350
val byteMap = Map[Byte, String](1.toByte -> "one", 2.toByte -> "two")
351
val shortMap = Map[Short, String](10.toShort -> "ten", 20.toShort -> "twenty")
352
val longMap = Map[Long, String](1000L -> "thousand", 2000L -> "two thousand")
353
val doubleMap = Map[Double, String](1.5 -> "one and half", 2.7 -> "two point seven")
354
355
// All encode to string keys
356
val byteJson = byteMap.asJson // {"1": "one", "2": "two"}
357
val shortJson = shortMap.asJson // {"10": "ten", "20": "twenty"}
358
val longJson = longMap.asJson // {"1000": "thousand", "2000": "two thousand"}
359
val doubleJson = doubleMap.asJson // {"1.5": "one and half", "2.7": "two point seven"}
360
361
// And can be decoded back
362
import io.circe.parser._
363
val decodedByteMap = parse(byteJson.noSpaces).flatMap(_.as[Map[Byte, String]])
364
val decodedShortMap = parse(shortJson.noSpaces).flatMap(_.as[Map[Short, String]])
365
val decodedLongMap = parse(longJson.noSpaces).flatMap(_.as[Map[Long, String]])
366
val decodedDoubleMap = parse(doubleJson.noSpaces).flatMap(_.as[Map[Double, String]])
367
```
368
369
### Syntax Extensions
370
371
```scala
372
import io.circe._
373
import io.circe.syntax._
374
375
case class ProductId(id: String)
376
implicit val productIdKeyEncoder: KeyEncoder[ProductId] = KeyEncoder[String].contramap(_.id)
377
378
val productId = ProductId("laptop-123")
379
val price = 999.99
380
381
// Using the := operator for key-value pairs
382
val keyValuePair: (String, Json) = productId := price
383
// Result: ("laptop-123", Json number 999.99)
384
385
// This is equivalent to:
386
val manualPair = (productIdKeyEncoder(productId), price.asJson)
387
```