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