0
# Annotations and Naming
1
2
Annotations for customizing JSON serialization behavior and naming strategies for automatic property name transformation during serialization and deserialization.
3
4
## Capabilities
5
6
### @JsonNames Annotation
7
8
Specify alternative property names for JSON deserialization, allowing flexible field name matching.
9
10
```kotlin { .api }
11
/**
12
* Specifies alternative names for JSON property deserialization
13
* Allows a single Kotlin property to match multiple JSON field names
14
*/
15
@Target(AnnotationTarget.PROPERTY)
16
@SerialInfo
17
@ExperimentalSerializationApi
18
annotation class JsonNames(vararg val names: String)
19
```
20
21
**Usage Examples:**
22
23
```kotlin
24
import kotlinx.serialization.*
25
import kotlinx.serialization.json.*
26
27
@Serializable
28
data class User(
29
@JsonNames("user_id", "userId", "ID")
30
val id: Int,
31
32
@JsonNames("user_name", "username", "displayName")
33
val name: String,
34
35
@JsonNames("email_address", "emailAddr", "mail")
36
val email: String
37
)
38
39
val json = Json {
40
useAlternativeNames = true // Must be enabled
41
}
42
43
// All of these JSON formats will deserialize to the same User object
44
val user1 = json.decodeFromString<User>("""{"user_id": 123, "user_name": "Alice", "email_address": "alice@example.com"}""")
45
val user2 = json.decodeFromString<User>("""{"userId": 123, "username": "Alice", "mail": "alice@example.com"}""")
46
val user3 = json.decodeFromString<User>("""{"ID": 123, "displayName": "Alice", "emailAddr": "alice@example.com"}""")
47
val user4 = json.decodeFromString<User>("""{"id": 123, "name": "Alice", "email": "alice@example.com"}""")
48
49
// All produce the same result: User(id=123, name="Alice", email="alice@example.com")
50
51
// Serialization always uses the primary property name
52
val serialized = json.encodeToString(user1)
53
// Result: {"id":123,"name":"Alice","email":"alice@example.com"}
54
```
55
56
**API Migration Example:**
57
58
```kotlin
59
import kotlinx.serialization.*
60
import kotlinx.serialization.json.*
61
62
// Handling API evolution with backward compatibility
63
@Serializable
64
data class ProductV2(
65
val id: Int,
66
67
@JsonNames("product_name", "title") // Legacy field names
68
val name: String,
69
70
@JsonNames("product_price", "cost", "amount")
71
val price: Double,
72
73
@JsonNames("product_category", "cat", "type")
74
val category: String,
75
76
@JsonNames("is_available", "available", "in_stock")
77
val isAvailable: Boolean = true
78
)
79
80
val json = Json { useAlternativeNames = true }
81
82
// Can handle old API format
83
val oldFormat = json.decodeFromString<ProductV2>("""
84
{
85
"id": 1,
86
"product_name": "Laptop",
87
"product_price": 999.99,
88
"product_category": "Electronics",
89
"is_available": true
90
}
91
""")
92
93
// Can handle new API format
94
val newFormat = json.decodeFromString<ProductV2>("""
95
{
96
"id": 1,
97
"title": "Laptop",
98
"cost": 999.99,
99
"type": "Electronics",
100
"in_stock": true
101
}
102
""")
103
```
104
105
### @JsonClassDiscriminator Annotation
106
107
Specify a custom discriminator property name for polymorphic serialization at the class level.
108
109
```kotlin { .api }
110
/**
111
* Specifies custom class discriminator property name for polymorphic serialization
112
* Applied to sealed classes or interfaces to override the default discriminator
113
*/
114
@Target(AnnotationTarget.CLASS)
115
@InheritableSerialInfo
116
@ExperimentalSerializationApi
117
annotation class JsonClassDiscriminator(val discriminator: String)
118
```
119
120
**Usage Examples:**
121
122
```kotlin
123
import kotlinx.serialization.*
124
import kotlinx.serialization.json.*
125
126
@Serializable
127
@JsonClassDiscriminator("messageType")
128
sealed class ChatMessage {
129
abstract val timestamp: Long
130
}
131
132
@Serializable
133
@SerialName("text")
134
data class TextMessage(
135
override val timestamp: Long,
136
val content: String,
137
val author: String
138
) : ChatMessage()
139
140
@Serializable
141
@SerialName("image")
142
data class ImageMessage(
143
override val timestamp: Long,
144
val imageUrl: String,
145
val caption: String?,
146
val author: String
147
) : ChatMessage()
148
149
@Serializable
150
@SerialName("system")
151
data class SystemMessage(
152
override val timestamp: Long,
153
val event: String,
154
val details: Map<String, String> = emptyMap()
155
) : ChatMessage()
156
157
val messages = listOf<ChatMessage>(
158
TextMessage(System.currentTimeMillis(), "Hello everyone!", "Alice"),
159
ImageMessage(System.currentTimeMillis(), "https://example.com/photo.jpg", "Beautiful sunset", "Bob"),
160
SystemMessage(System.currentTimeMillis(), "USER_JOINED", mapOf("username" to "Charlie"))
161
)
162
163
val json = Json.encodeToString(messages)
164
// Result uses "messageType" instead of default "type":
165
// [
166
// {"messageType":"text","timestamp":1234567890,"content":"Hello everyone!","author":"Alice"},
167
// {"messageType":"image","timestamp":1234567891,"imageUrl":"https://example.com/photo.jpg","caption":"Beautiful sunset","author":"Bob"},
168
// {"messageType":"system","timestamp":1234567892,"event":"USER_JOINED","details":{"username":"Charlie"}}
169
// ]
170
171
val decoded = Json.decodeFromString<List<ChatMessage>>(json)
172
```
173
174
**Nested Class Discriminators:**
175
176
```kotlin
177
import kotlinx.serialization.*
178
import kotlinx.serialization.json.*
179
180
@Serializable
181
@JsonClassDiscriminator("shapeType")
182
sealed class Shape {
183
abstract val area: Double
184
}
185
186
@Serializable
187
@SerialName("circle")
188
data class Circle(val radius: Double) : Shape() {
189
override val area: Double get() = 3.14159 * radius * radius
190
}
191
192
@Serializable
193
@JsonClassDiscriminator("vehicleKind")
194
sealed class Vehicle {
195
abstract val maxSpeed: Int
196
}
197
198
@Serializable
199
@SerialName("car")
200
data class Car(override val maxSpeed: Int, val doors: Int) : Vehicle()
201
202
@Serializable
203
data class DrawingObject(
204
val id: String,
205
val shape: Shape,
206
val vehicle: Vehicle? = null
207
)
208
209
// Each polymorphic hierarchy uses its own discriminator
210
val obj = DrawingObject(
211
"obj1",
212
Circle(5.0),
213
Car(120, 4)
214
)
215
216
val json = Json.encodeToString(obj)
217
// Result:
218
// {
219
// "id": "obj1",
220
// "shape": {"shapeType": "circle", "radius": 5.0},
221
// "vehicle": {"vehicleKind": "car", "maxSpeed": 120, "doors": 4}
222
// }
223
```
224
225
### @JsonIgnoreUnknownKeys Annotation
226
227
Ignore unknown JSON properties for a specific class during deserialization, overriding the global Json configuration.
228
229
```kotlin { .api }
230
/**
231
* Ignore unknown properties during deserialization for annotated class
232
* Overrides the global ignoreUnknownKeys setting for this specific class
233
*/
234
@Target(AnnotationTarget.CLASS)
235
@SerialInfo
236
@ExperimentalSerializationApi
237
annotation class JsonIgnoreUnknownKeys
238
```
239
240
**Usage Examples:**
241
242
```kotlin
243
import kotlinx.serialization.*
244
import kotlinx.serialization.json.*
245
246
// Strict class - will fail on unknown properties
247
@Serializable
248
data class StrictConfig(
249
val timeout: Int,
250
val enabled: Boolean
251
)
252
253
// Flexible class - ignores unknown properties
254
@Serializable
255
@JsonIgnoreUnknownKeys
256
data class FlexibleConfig(
257
val timeout: Int,
258
val enabled: Boolean
259
)
260
261
@Serializable
262
data class AppSettings(
263
val strict: StrictConfig,
264
val flexible: FlexibleConfig
265
)
266
267
// Global Json configuration is strict
268
val json = Json {
269
ignoreUnknownKeys = false // Strict by default
270
}
271
272
val jsonString = """
273
{
274
"strict": {
275
"timeout": 30,
276
"enabled": true,
277
"extra": "this will cause error"
278
},
279
"flexible": {
280
"timeout": 60,
281
"enabled": false,
282
"extra": "this will be ignored",
283
"moreExtra": "this too"
284
}
285
}
286
"""
287
288
try {
289
val settings = json.decodeFromString<AppSettings>(jsonString)
290
// This will fail because StrictConfig doesn't ignore unknown keys
291
} catch (e: SerializationException) {
292
println("Error: ${e.message}") // Unknown key 'extra'
293
}
294
295
// Fix the JSON for strict config
296
val fixedJsonString = """
297
{
298
"strict": {
299
"timeout": 30,
300
"enabled": true
301
},
302
"flexible": {
303
"timeout": 60,
304
"enabled": false,
305
"extra": "this will be ignored",
306
"moreExtra": "this too"
307
}
308
}
309
"""
310
311
val settings = json.decodeFromString<AppSettings>(fixedJsonString)
312
// Success: FlexibleConfig ignores extra fields, StrictConfig has no extra fields
313
```
314
315
### JsonNamingStrategy Interface
316
317
Strategy interface for automatic property name transformation during serialization.
318
319
```kotlin { .api }
320
/**
321
* Strategy for transforming property names during JSON serialization
322
* Provides automatic name transformation without manual field annotations
323
*/
324
@ExperimentalSerializationApi
325
fun interface JsonNamingStrategy {
326
/**
327
* Transform a property name for JSON serialization
328
* @param descriptor Serial descriptor of the containing class
329
* @param elementIndex Index of the property in the descriptor
330
* @param serialName Original property name from Kotlin
331
* @return Transformed property name for JSON
332
*/
333
fun serialNameForJson(
334
descriptor: SerialDescriptor,
335
elementIndex: Int,
336
serialName: String
337
): String
338
339
companion object {
340
/**
341
* Converts camelCase property names to snake_case
342
*/
343
val SnakeCase: JsonNamingStrategy
344
345
/**
346
* Converts camelCase property names to kebab-case
347
*/
348
val KebabCase: JsonNamingStrategy
349
}
350
}
351
```
352
353
**Built-in Strategies:**
354
355
```kotlin
356
import kotlinx.serialization.*
357
import kotlinx.serialization.json.*
358
359
@Serializable
360
data class UserPreferences(
361
val darkModeEnabled: Boolean,
362
val autoSaveInterval: Int,
363
val notificationSettings: NotificationSettings,
364
val preferredLanguage: String
365
)
366
367
@Serializable
368
data class NotificationSettings(
369
val emailNotifications: Boolean,
370
val pushNotifications: Boolean,
371
val soundEnabled: Boolean
372
)
373
374
val preferences = UserPreferences(
375
darkModeEnabled = true,
376
autoSaveInterval = 300,
377
notificationSettings = NotificationSettings(
378
emailNotifications = true,
379
pushNotifications = false,
380
soundEnabled = true
381
),
382
preferredLanguage = "en-US"
383
)
384
385
// Snake case transformation
386
val snakeCaseJson = Json {
387
namingStrategy = JsonNamingStrategy.SnakeCase
388
}
389
val snakeCase = snakeCaseJson.encodeToString(preferences)
390
// Result:
391
// {
392
// "dark_mode_enabled": true,
393
// "auto_save_interval": 300,
394
// "notification_settings": {
395
// "email_notifications": true,
396
// "push_notifications": false,
397
// "sound_enabled": true
398
// },
399
// "preferred_language": "en-US"
400
// }
401
402
// Kebab case transformation
403
val kebabCaseJson = Json {
404
namingStrategy = JsonNamingStrategy.KebabCase
405
}
406
val kebabCase = kebabCaseJson.encodeToString(preferences)
407
// Result:
408
// {
409
// "dark-mode-enabled": true,
410
// "auto-save-interval": 300,
411
// "notification-settings": {
412
// "email-notifications": true,
413
// "push-notifications": false,
414
// "sound-enabled": true
415
// },
416
// "preferred-language": "en-US"
417
// }
418
419
// Deserialization works with the same naming strategy
420
val decodedSnake = snakeCaseJson.decodeFromString<UserPreferences>(snakeCase)
421
val decodedKebab = kebabCaseJson.decodeFromString<UserPreferences>(kebabCase)
422
```
423
424
**Custom Naming Strategy:**
425
426
```kotlin
427
import kotlinx.serialization.*
428
import kotlinx.serialization.json.*
429
import kotlinx.serialization.descriptors.*
430
431
// Custom strategy: UPPERCASE property names
432
object UpperCaseNamingStrategy : JsonNamingStrategy {
433
override fun serialNameForJson(
434
descriptor: SerialDescriptor,
435
elementIndex: Int,
436
serialName: String
437
): String = serialName.uppercase()
438
}
439
440
// Custom strategy: Add prefix based on class name
441
class PrefixNamingStrategy(private val prefix: String) : JsonNamingStrategy {
442
override fun serialNameForJson(
443
descriptor: SerialDescriptor,
444
elementIndex: Int,
445
serialName: String
446
): String = "${prefix}_$serialName"
447
}
448
449
// Custom strategy: Conditional transformation
450
object ApiNamingStrategy : JsonNamingStrategy {
451
override fun serialNameForJson(
452
descriptor: SerialDescriptor,
453
elementIndex: Int,
454
serialName: String
455
): String {
456
return when {
457
serialName.endsWith("Id") -> serialName.lowercase()
458
serialName.startsWith("is") -> serialName.removePrefix("is").lowercase()
459
serialName.contains("Url") -> serialName.replace("Url", "URL")
460
else -> serialName.lowercase()
461
}
462
}
463
}
464
465
@Serializable
466
data class ApiUser(
467
val userId: Int,
468
val isActive: Boolean,
469
val profileUrl: String,
470
val displayName: String
471
)
472
473
val user = ApiUser(123, true, "https://example.com/profile.jpg", "Alice")
474
475
val customJson = Json {
476
namingStrategy = ApiNamingStrategy
477
}
478
val result = customJson.encodeToString(user)
479
// Result: {"userid":123,"active":true,"profileURL":"https://example.com/profile.jpg","displayname":"Alice"}
480
```
481
482
### Combining Annotations and Naming Strategies
483
484
Annotations take precedence over naming strategies for specific properties.
485
486
```kotlin
487
import kotlinx.serialization.*
488
import kotlinx.serialization.json.*
489
490
@Serializable
491
data class MixedNamingExample(
492
// Uses naming strategy transformation
493
val firstName: String,
494
495
// Override with explicit @SerialName
496
@SerialName("family_name")
497
val lastName: String,
498
499
// Override with @JsonNames for flexibility
500
@JsonNames("user_email", "email_addr")
501
val emailAddress: String,
502
503
// Uses naming strategy transformation
504
val phoneNumber: String?
505
)
506
507
val json = Json {
508
namingStrategy = JsonNamingStrategy.SnakeCase
509
useAlternativeNames = true
510
}
511
512
val person = MixedNamingExample(
513
"John",
514
"Doe",
515
"john@example.com",
516
"555-1234"
517
)
518
519
val encoded = json.encodeToString(person)
520
// Result: {
521
// "first_name": "John", // Transformed by naming strategy
522
// "family_name": "Doe", // Uses explicit @SerialName
523
// "email_address": "john@example.com", // Uses property name (not alternative names in serialization)
524
// "phone_number": "555-1234" // Transformed by naming strategy
525
// }
526
527
// Can decode using alternative names
528
val alternativeJson = """
529
{
530
"first_name": "Jane",
531
"family_name": "Smith",
532
"user_email": "jane@example.com",
533
"phone_number": "555-5678"
534
}
535
"""
536
537
val decoded = json.decodeFromString<MixedNamingExample>(alternativeJson)
538
// Success: uses "user_email" alternative name for emailAddress
539
```
540
541
## Best Practices
542
543
### When to Use Each Annotation
544
545
- **@JsonNames**: API evolution, supporting multiple client versions, integrating with inconsistent third-party APIs
546
- **@JsonClassDiscriminator**: Domain-specific discriminator names that are more meaningful than "type"
547
- **@JsonIgnoreUnknownKeys**: Classes that need to be forward-compatible with API changes
548
- **JsonNamingStrategy**: Consistent naming convention across entire API without manual annotations
549
550
### Performance Considerations
551
552
- **@JsonNames**: Minimal performance impact, names are resolved at compile time
553
- **JsonNamingStrategy**: Applied to every property during serialization/deserialization
554
- **@JsonIgnoreUnknownKeys**: No performance impact, just skips unknown properties
555
- **@JsonClassDiscriminator**: No performance impact, uses standard polymorphic serialization