or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

cursor-navigation.mderror-handling.mdindex.mdjson-data-types.mdjson-printing.mdkey-encoding-decoding.mdtype-classes.md

key-encoding-decoding.mddocs/

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

```