or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced.mdannotations.mdbuilders.mdconfiguration.mdindex.mdjson-element.mdplatform.mdserialization.md

annotations.mddocs/

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