or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

annotations.mdconfiguration.mdcore-operations.mdcustom-serializers.mddsl-builders.mdindex.mdjson-elements.mdnaming-strategies.mdplatform-extensions.md

custom-serializers.mddocs/

0

# Custom Serializers

1

2

Interfaces and base classes for implementing custom JSON serialization logic with access to JsonElement representations.

3

4

## Capabilities

5

6

### JsonEncoder Interface

7

8

Encoder interface providing access to Json instance and JsonElement encoding capability.

9

10

```kotlin { .api }

11

/**

12

* Encoder used by Json during serialization

13

* Provides access to Json instance and direct JsonElement encoding

14

*/

15

interface JsonEncoder : Encoder, CompositeEncoder {

16

/** An instance of the current Json */

17

val json: Json

18

19

/**

20

* Appends the given JSON element to the current output

21

* This method should only be used as part of the whole serialization process

22

* @param element JsonElement to encode directly

23

*/

24

fun encodeJsonElement(element: JsonElement)

25

}

26

```

27

28

**Usage Examples:**

29

30

```kotlin

31

@Serializable(with = CustomDataSerializer::class)

32

data class CustomData(val value: String, val metadata: Map<String, Any>)

33

34

object CustomDataSerializer : KSerializer<CustomData> {

35

override val descriptor = buildClassSerialDescriptor("CustomData") {

36

element<String>("value")

37

element<JsonElement>("metadata")

38

}

39

40

override fun serialize(encoder: Encoder, value: CustomData) {

41

val output = encoder as? JsonEncoder

42

?: throw SerializationException("This serializer can only be used with Json format")

43

44

val element = buildJsonObject {

45

put("value", value.value)

46

put("timestamp", System.currentTimeMillis())

47

48

putJsonObject("metadata") {

49

value.metadata.forEach { (key, metaValue) ->

50

when (metaValue) {

51

is String -> put(key, metaValue)

52

is Number -> put(key, metaValue)

53

is Boolean -> put(key, metaValue)

54

else -> put(key, metaValue.toString())

55

}

56

}

57

}

58

}

59

60

output.encodeJsonElement(element)

61

}

62

63

override fun deserialize(decoder: Decoder): CustomData {

64

val input = decoder as? JsonDecoder

65

?: throw SerializationException("This serializer can only be used with Json format")

66

67

val element = input.decodeJsonElement().jsonObject

68

val value = element["value"]?.jsonPrimitive?.content

69

?: throw SerializationException("Missing 'value' field")

70

71

val metadata = element["metadata"]?.jsonObject?.mapValues { (_, jsonValue) ->

72

when (jsonValue) {

73

is JsonPrimitive -> when {

74

jsonValue.isString -> jsonValue.content

75

jsonValue.booleanOrNull != null -> jsonValue.boolean

76

jsonValue.longOrNull != null -> jsonValue.long

77

jsonValue.doubleOrNull != null -> jsonValue.double

78

else -> jsonValue.content

79

}

80

else -> jsonValue.toString()

81

}

82

} ?: emptyMap()

83

84

return CustomData(value, metadata)

85

}

86

}

87

```

88

89

### JsonDecoder Interface

90

91

Decoder interface providing access to Json instance and JsonElement decoding capability.

92

93

```kotlin { .api }

94

/**

95

* Decoder used by Json during deserialization

96

* Provides access to Json instance and direct JsonElement decoding

97

*/

98

interface JsonDecoder : Decoder, CompositeDecoder {

99

/** An instance of the current Json */

100

val json: Json

101

102

/**

103

* Decodes the next element in the current input as JsonElement

104

* This method should only be used as part of the whole deserialization process

105

* @return JsonElement representation of current input

106

*/

107

fun decodeJsonElement(): JsonElement

108

}

109

```

110

111

**Usage Examples:**

112

113

```kotlin

114

// Conditional deserialization based on JSON content

115

@Serializable(with = FlexibleResponseSerializer::class)

116

sealed class ApiResponse {

117

@Serializable

118

data class Success(val data: JsonElement) : ApiResponse()

119

120

@Serializable

121

data class Error(val message: String, val code: Int) : ApiResponse()

122

}

123

124

object FlexibleResponseSerializer : KSerializer<ApiResponse> {

125

override val descriptor = buildSerialDescriptor("ApiResponse", PolymorphicKind.SEALED)

126

127

override fun serialize(encoder: Encoder, value: ApiResponse) {

128

val output = encoder as JsonEncoder

129

val element = when (value) {

130

is ApiResponse.Success -> buildJsonObject {

131

put("success", true)

132

put("data", value.data)

133

}

134

is ApiResponse.Error -> buildJsonObject {

135

put("success", false)

136

put("error", buildJsonObject {

137

put("message", value.message)

138

put("code", value.code)

139

})

140

}

141

}

142

output.encodeJsonElement(element)

143

}

144

145

override fun deserialize(decoder: Decoder): ApiResponse {

146

val input = decoder as JsonDecoder

147

val element = input.decodeJsonElement().jsonObject

148

149

val success = element["success"]?.jsonPrimitive?.boolean ?: false

150

151

return if (success) {

152

val data = element["data"] ?: JsonNull

153

ApiResponse.Success(data)

154

} else {

155

val errorObj = element["error"]?.jsonObject

156

?: throw SerializationException("Missing error object")

157

val message = errorObj["message"]?.jsonPrimitive?.content

158

?: throw SerializationException("Missing error message")

159

val code = errorObj["code"]?.jsonPrimitive?.int

160

?: throw SerializationException("Missing error code")

161

ApiResponse.Error(message, code)

162

}

163

}

164

}

165

166

// Usage

167

val json = Json { ignoreUnknownKeys = true }

168

169

val successJson = """{"success":true,"data":{"user":"Alice","score":100}}"""

170

val errorJson = """{"success":false,"error":{"message":"Not found","code":404}}"""

171

172

val successResponse = json.decodeFromString<ApiResponse>(successJson)

173

val errorResponse = json.decodeFromString<ApiResponse>(errorJson)

174

```

175

176

### JsonTransformingSerializer

177

178

Abstract base class for serializers that transform JsonElement during serialization/deserialization.

179

180

```kotlin { .api }

181

/**

182

* Base class for custom serializers that manipulate JsonElement representation

183

* before serialization or after deserialization

184

* @param T Type for Kotlin property this serializer applies to

185

* @param tSerializer Serializer for type T

186

*/

187

abstract class JsonTransformingSerializer<T : Any?>(

188

private val tSerializer: KSerializer<T>

189

) : KSerializer<T> {

190

191

/** Descriptor delegates to tSerializer's descriptor by default */

192

override val descriptor: SerialDescriptor get() = tSerializer.descriptor

193

194

/**

195

* Transformation applied during deserialization

196

* JsonElement from input is transformed before being passed to tSerializer

197

* @param element Original JsonElement from input

198

* @return Transformed JsonElement for deserialization

199

*/

200

protected open fun transformDeserialize(element: JsonElement): JsonElement = element

201

202

/**

203

* Transformation applied during serialization

204

* JsonElement from tSerializer is transformed before output

205

* @param element JsonElement produced by tSerializer

206

* @return Transformed JsonElement for output

207

*/

208

protected open fun transformSerialize(element: JsonElement): JsonElement = element

209

}

210

```

211

212

**Usage Examples:**

213

214

```kotlin

215

// Transform list to single object and vice versa

216

@Serializable

217

data class UserPreferences(@Serializable(UnwrappingListSerializer::class) val theme: String)

218

219

object UnwrappingListSerializer : JsonTransformingSerializer<String>(String.serializer()) {

220

override fun transformDeserialize(element: JsonElement): JsonElement {

221

// If input is array with single element, unwrap it

222

return if (element is JsonArray && element.size == 1) {

223

element.first()

224

} else {

225

element

226

}

227

}

228

229

override fun transformSerialize(element: JsonElement): JsonElement {

230

// Wrap single string in array for output

231

return buildJsonArray { add(element) }

232

}

233

}

234

235

// Usage: Both inputs deserialize to same result

236

val prefs1 = json.decodeFromString<UserPreferences>("""{"theme":"dark"}""")

237

val prefs2 = json.decodeFromString<UserPreferences>("""{"theme":["dark"]}""")

238

// Both create UserPreferences(theme="dark")

239

240

// But serialization always produces array format

241

val output = json.encodeToString(prefs1)

242

// {"theme":["dark"]}

243

244

// Normalize number formats

245

object NormalizedNumberSerializer : JsonTransformingSerializer<Double>(Double.serializer()) {

246

override fun transformDeserialize(element: JsonElement): JsonElement {

247

// Accept both string and number representations

248

return when (element) {

249

is JsonPrimitive -> {

250

if (element.isString) {

251

val number = element.content.toDoubleOrNull()

252

if (number != null) JsonPrimitive(number) else element

253

} else element

254

}

255

else -> element

256

}

257

}

258

259

override fun transformSerialize(element: JsonElement): JsonElement {

260

// Always output as number, never as string

261

return if (element is JsonPrimitive && element.isString) {

262

val number = element.content.toDoubleOrNull()

263

if (number != null) JsonPrimitive(number) else element

264

} else element

265

}

266

}

267

268

@Serializable

269

data class Measurement(@Serializable(NormalizedNumberSerializer::class) val value: Double)

270

271

// Accepts both formats

272

val measurement1 = json.decodeFromString<Measurement>("""{"value":123.45}""")

273

val measurement2 = json.decodeFromString<Measurement>("""{"value":"123.45"}""")

274

// Both create Measurement(value=123.45)

275

276

// Always outputs as number

277

val output = json.encodeToString(measurement1)

278

// {"value":123.45}

279

```

280

281

### JsonContentPolymorphicSerializer

282

283

Abstract base class for polymorphic serializers that select deserializer based on JSON content.

284

285

```kotlin { .api }

286

/**

287

* Base class for custom serializers that select polymorphic serializer

288

* based on JSON content rather than class discriminator

289

* @param T Root type for polymorphic class hierarchy

290

* @param baseClass Class token for T

291

*/

292

abstract class JsonContentPolymorphicSerializer<T : Any>(

293

private val baseClass: KClass<T>

294

) : KSerializer<T> {

295

296

/** Descriptor with polymorphic kind */

297

override val descriptor: SerialDescriptor

298

299

/**

300

* Determines deserialization strategy by examining parsed JSON element

301

* @param element JsonElement to examine for determining type

302

* @return DeserializationStrategy for the appropriate subtype

303

*/

304

protected abstract fun selectDeserializer(element: JsonElement): DeserializationStrategy<T>

305

}

306

```

307

308

**Usage Examples:**

309

310

```kotlin

311

@Serializable

312

sealed class PaymentMethod {

313

@Serializable

314

data class CreditCard(val number: String, val expiry: String) : PaymentMethod()

315

316

@Serializable

317

data class BankTransfer(val accountNumber: String, val routingNumber: String) : PaymentMethod()

318

319

@Serializable

320

data class DigitalWallet(val walletId: String, val provider: String) : PaymentMethod()

321

}

322

323

object PaymentMethodSerializer : JsonContentPolymorphicSerializer<PaymentMethod>(PaymentMethod::class) {

324

override fun selectDeserializer(element: JsonElement): DeserializationStrategy<PaymentMethod> {

325

val obj = element.jsonObject

326

return when {

327

"number" in obj && "expiry" in obj -> PaymentMethod.CreditCard.serializer()

328

"accountNumber" in obj && "routingNumber" in obj -> PaymentMethod.BankTransfer.serializer()

329

"walletId" in obj && "provider" in obj -> PaymentMethod.DigitalWallet.serializer()

330

else -> throw SerializationException("Unknown payment method type")

331

}

332

}

333

}

334

335

// Register the serializer

336

val json = Json {

337

serializersModule = SerializersModule {

338

polymorphic(PaymentMethod::class) {

339

default { PaymentMethodSerializer }

340

}

341

}

342

}

343

344

// Usage - no type discriminator needed in JSON

345

val creditCardJson = """{"number":"1234-5678-9012-3456","expiry":"12/25"}"""

346

val bankTransferJson = """{"accountNumber":"123456789","routingNumber":"987654321"}"""

347

val walletJson = """{"walletId":"user123","provider":"PayPal"}"""

348

349

val creditCard = json.decodeFromString<PaymentMethod>(creditCardJson)

350

val bankTransfer = json.decodeFromString<PaymentMethod>(bankTransferJson)

351

val wallet = json.decodeFromString<PaymentMethod>(walletJson)

352

353

// Complex content-based selection

354

@Serializable

355

sealed class DatabaseConfig {

356

@Serializable

357

data class MySQL(val host: String, val port: Int = 3306, val charset: String = "utf8") : DatabaseConfig()

358

359

@Serializable

360

data class PostgreSQL(val host: String, val port: Int = 5432, val schema: String = "public") : DatabaseConfig()

361

362

@Serializable

363

data class MongoDB(val connectionString: String, val database: String) : DatabaseConfig()

364

}

365

366

object DatabaseConfigSerializer : JsonContentPolymorphicSerializer<DatabaseConfig>(DatabaseConfig::class) {

367

override fun selectDeserializer(element: JsonElement): DeserializationStrategy<DatabaseConfig> {

368

val obj = element.jsonObject

369

return when {

370

"connectionString" in obj -> DatabaseConfig.MongoDB.serializer()

371

"charset" in obj -> DatabaseConfig.MySQL.serializer()

372

"schema" in obj -> DatabaseConfig.PostgreSQL.serializer()

373

obj["port"]?.jsonPrimitive?.int == 3306 -> DatabaseConfig.MySQL.serializer()

374

obj["port"]?.jsonPrimitive?.int == 5432 -> DatabaseConfig.PostgreSQL.serializer()

375

else -> DatabaseConfig.PostgreSQL.serializer() // Default

376

}

377

}

378

}

379

```