Skip to content

Commit 1b69687

Browse files
Added documentation for @KeepGeneratedSerializer feature (#2669)
Resolves #2660 Co-authored-by: Leonid Startsev <[email protected]>
1 parent ee5bf5e commit 1b69687

14 files changed

+319
-128
lines changed

core/commonMain/src/kotlinx/serialization/Annotations.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import kotlin.reflect.*
3131
* MyAnotherData.serializer() // <- returns MyAnotherDataCustomSerializer
3232
* ```
3333
*
34+
* To continue generating the implementation of [KSerializer] using the plugin, specify the [KeepGeneratedSerializer] annotation.
35+
* In this case, the serializer will be available via `generatedSerializer()` function, and will also be used in the heirs.
36+
*
3437
* For annotated properties, specifying [with] parameter is mandatory and can be used to override
3538
* serializer on the use-site without affecting the rest of the usages:
3639
* ```
@@ -64,6 +67,7 @@ import kotlin.reflect.*
6467
*
6568
* @see UseSerializers
6669
* @see Serializer
70+
* @see KeepGeneratedSerializer
6771
*/
6872
@MustBeDocumented
6973
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS, AnnotationTarget.TYPE)
@@ -330,13 +334,15 @@ public annotation class Polymorphic
330334
*
331335
* Automatically generated serializer is available via `generatedSerializer()` function in companion object of serializable class.
332336
*
333-
* Generated serializers allow to use custom serializers on classes from which other serializable classes are inherited.
337+
* Keeping generated serializers allow to use plugin generated serializer in inheritors even if custom serializer is specified.
338+
*
339+
* Used only with annotation [Serializable] with the specified argument [Serializable.with], e.g. `@Serializable(with=SomeSerializer::class)`.
334340
*
335-
* Used only with the [Serializable] annotation.
341+
* Annotation is not allowed on classes involved in polymorphic serialization:
342+
* interfaces, sealed classes, abstract classes, classes marked by [Polymorphic].
336343
*
337-
* A compiler version `2.0.0` and higher is required.
344+
* A compiler version `2.0.20` and higher is required.
338345
*/
339-
@InternalSerializationApi
340346
@Target(AnnotationTarget.CLASS)
341347
@Retention(AnnotationRetention.RUNTIME)
342348
public annotation class KeepGeneratedSerializer

docs/json.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json
3737
* [Array unwrapping](#array-unwrapping)
3838
* [Manipulating default values](#manipulating-default-values)
3939
* [Content-based polymorphic deserialization](#content-based-polymorphic-deserialization)
40+
* [Extending the behavior of the plugin generated serializer](#extending-the-behavior-of-the-plugin-generated-serializer)
4041
* [Under the hood (experimental)](#under-the-hood-experimental)
4142
* [Maintaining custom JSON attributes](#maintaining-custom-json-attributes)
4243

@@ -1260,6 +1261,53 @@ No class discriminator is added in the JSON output:
12601261

12611262
<!--- TEST -->
12621263

1264+
### Extending the behavior of the plugin generated serializer
1265+
In some cases, it may be necessary to add additional serialization logic on top of the plugin generated logic.
1266+
For example, to add a preliminary modification of JSON elements or to add processing of unknown values of enums.
1267+
1268+
In this case, you can mark the serializable class with the [`@KeepGeneratedSerializer`][KeepGeneratedSerializer] annotation and get the generated serializer using the `generatedSerializer()` function.
1269+
1270+
Here is an example of the simultaneous use of [JsonTransformingSerializer] and polymorphism.
1271+
In this example, we use `transformDeserialize` function to rename `basic-name` key into `name` so it matches the `abstract val name` property from the `Project` supertype.
1272+
```kotlin
1273+
@Serializable
1274+
sealed class Project {
1275+
abstract val name: String
1276+
}
1277+
1278+
@KeepGeneratedSerializer
1279+
@Serializable(with = BasicProjectSerializer::class)
1280+
@SerialName("basic")
1281+
data class BasicProject(override val name: String): Project()
1282+
1283+
object BasicProjectSerializer : JsonTransformingSerializer<BasicProject>(BasicProject.generatedSerializer()) {
1284+
override fun transformDeserialize(element: JsonElement): JsonElement {
1285+
val jsonObject = element.jsonObject
1286+
return if ("basic-name" in jsonObject) {
1287+
val nameElement = jsonObject["basic-name"] ?: throw IllegalStateException()
1288+
JsonObject(mapOf("name" to nameElement))
1289+
} else {
1290+
jsonObject
1291+
}
1292+
}
1293+
}
1294+
1295+
1296+
fun main() {
1297+
val project = Json.decodeFromString<Project>("""{"type":"basic","basic-name":"example"}""")
1298+
println(project)
1299+
}
1300+
```
1301+
1302+
> You can get the full code [here](../guide/example/example-json-29.kt).
1303+
1304+
`BasicProject` will be printed to the output:
1305+
1306+
```text
1307+
BasicProject(name=example)
1308+
```
1309+
<!--- TEST -->
1310+
12631311
### Under the hood (experimental)
12641312

12651313
Although abstract serializers mentioned above can cover most of the cases, it is possible to implement similar machinery
@@ -1345,7 +1393,7 @@ fun main() {
13451393
}
13461394
```
13471395

1348-
> You can get the full code [here](../guide/example/example-json-29.kt).
1396+
> You can get the full code [here](../guide/example/example-json-30.kt).
13491397
13501398
This gives you fine-grained control on the representation of the `Response` class in the JSON output:
13511399

@@ -1410,7 +1458,7 @@ fun main() {
14101458
}
14111459
```
14121460

1413-
> You can get the full code [here](../guide/example/example-json-30.kt).
1461+
> You can get the full code [here](../guide/example/example-json-31.kt).
14141462
14151463
```text
14161464
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
@@ -1440,6 +1488,7 @@ The next chapter covers [Alternative and custom formats (experimental)](formats.
14401488
[InheritableSerialInfo]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-inheritable-serial-info/index.html
14411489
[KSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/index.html
14421490
[Serializable]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serializable/index.html
1491+
[KeepGeneratedSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-keep-generated-serializer/index.html
14431492

14441493
<!--- INDEX kotlinx-serialization-core/kotlinx.serialization.encoding -->
14451494

docs/serialization-guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Once the project is set up, we can start serializing some classes.
7676
* <a name='specifying-serializer-globally-using-typealias'></a>[Specifying serializer globally using typealias](serializers.md#specifying-serializer-globally-using-typealias)
7777
* <a name='custom-serializers-for-a-generic-type'></a>[Custom serializers for a generic type](serializers.md#custom-serializers-for-a-generic-type)
7878
* <a name='format-specific-serializers'></a>[Format-specific serializers](serializers.md#format-specific-serializers)
79+
* <a name='simultaneous-use-of-plugin-generated-and-custom-serializers'></a>[Simultaneous use of plugin-generated and custom serializers](serializers.md#simultaneous-use-of-plugin-generated-and-custom-serializers)
7980
* <a name='contextual-serialization'></a>[Contextual serialization](serializers.md#contextual-serialization)
8081
* <a name='serializers-module'></a>[Serializers module](serializers.md#serializers-module)
8182
* <a name='contextual-serialization-and-generic-classes'></a>[Contextual serialization and generic classes](serializers.md#contextual-serialization-and-generic-classes)
@@ -137,6 +138,7 @@ Once the project is set up, we can start serializing some classes.
137138
* <a name='array-unwrapping'></a>[Array unwrapping](json.md#array-unwrapping)
138139
* <a name='manipulating-default-values'></a>[Manipulating default values](json.md#manipulating-default-values)
139140
* <a name='content-based-polymorphic-deserialization'></a>[Content-based polymorphic deserialization](json.md#content-based-polymorphic-deserialization)
141+
* <a name='extending-the-behavior-of-the-plugin-generated-serializer'></a>[Extending the behavior of the plugin generated serializer](json.md#extending-the-behavior-of-the-plugin-generated-serializer)
140142
* <a name='under-the-hood-experimental'></a>[Under the hood (experimental)](json.md#under-the-hood-experimental)
141143
* <a name='maintaining-custom-json-attributes'></a>[Maintaining custom JSON attributes](json.md#maintaining-custom-json-attributes)
142144
<!--- END -->

docs/serializers.md

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ In this chapter we'll take a look at serializers in more detail, and we'll see h
2929
* [Specifying serializer globally using typealias](#specifying-serializer-globally-using-typealias)
3030
* [Custom serializers for a generic type](#custom-serializers-for-a-generic-type)
3131
* [Format-specific serializers](#format-specific-serializers)
32+
* [Simultaneous use of plugin-generated and custom serializers](#simultaneous-use-of-plugin-generated-and-custom-serializers)
3233
* [Contextual serialization](#contextual-serialization)
3334
* [Serializers module](#serializers-module)
3435
* [Contextual serialization and generic classes](#contextual-serialization-and-generic-classes)
@@ -810,7 +811,7 @@ fun main() {
810811

811812
<!--- TEST -->
812813

813-
### Specifying serializers for a file
814+
### Specifying serializers for a file
814815

815816
A serializer for a specific type, like `Date`, can be specified for a whole source code file with the file-level
816817
[UseSerializers] annotation at the beginning of the file.
@@ -975,6 +976,58 @@ features that a serializer implementation would like to take advantage of.
975976
976977
This chapter proceeds with a generic approach to tweaking the serialization strategy based on the context.
977978
979+
## Simultaneous use of plugin-generated and custom serializers
980+
In some cases it may be useful to have a serialization plugin continue to generate a serializer even if a custom one is used for the class.
981+
982+
The most common examples are: using a plugin-generated serializer for fallback strategy, accessing type structure via [descriptor][KSerializer.descriptor] of plugin-generated serializer, use default serialization behavior in descendants that do not use custom serializers.
983+
984+
In order for the plugin to continue generating the serializer, you must specify the `@KeepGeneratedSerializer` annotation in the type declaration.
985+
In this case, the serializer will be accessible using the `.generatedSerializer()` function on the class's companion object.
986+
987+
Annotation `@KeepGeneratedSerializer` is not allowed on classes involved in polymorphic serialization: interfaces, sealed classes, abstract classes, classes marked by [Polymorphic].
988+
989+
An example of using two serializers at once:
990+
991+
<!--- INCLUDE
992+
object ColorAsStringSerializer : KSerializer<Color> {
993+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING)
994+
995+
override fun serialize(encoder: Encoder, value: Color) {
996+
val string = value.rgb.toString(16).padStart(6, '0')
997+
encoder.encodeString(string)
998+
}
999+
1000+
override fun deserialize(decoder: Decoder): Color {
1001+
val string = decoder.decodeString()
1002+
return Color(string.toInt(16))
1003+
}
1004+
}
1005+
-->
1006+
1007+
```kotlin
1008+
@KeepGeneratedSerializer
1009+
@Serializable(with = ColorAsStringSerializer::class)
1010+
class Color(val rgb: Int)
1011+
1012+
1013+
fun main() {
1014+
val green = Color(0x00ff00)
1015+
println(Json.encodeToString(green))
1016+
println(Json.encodeToString(Color.generatedSerializer(), green))
1017+
}
1018+
```
1019+
1020+
> You can get the full code [here](../guide/example/example-serializer-20.kt).
1021+
1022+
As a result, serialization will occur using custom and plugin-generated serializers:
1023+
1024+
```text
1025+
"00ff00"
1026+
{"rgb":65280}
1027+
```
1028+
1029+
<!--- TEST -->
1030+
9781031
## Contextual serialization
9791032

9801033
All the previous approaches to specifying custom serialization strategies were _static_, that is
@@ -1014,7 +1067,7 @@ fun main() {
10141067
To actually serialize this class we must provide the corresponding context when calling the `encodeToXxx`/`decodeFromXxx`
10151068
functions. Without it we'll get a "Serializer for class 'Date' is not found" exception.
10161069
1017-
> See [here](../guide/example/example-serializer-20.kt) for an example that produces that exception.
1070+
> See [here](../guide/example/example-serializer-21.kt) for an example that produces that exception.
10181071
10191072
<!--- TEST LINES_START
10201073
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Date' is not found.
@@ -1073,7 +1126,7 @@ fun main() {
10731126
}
10741127
```
10751128
1076-
> You can get the full code [here](../guide/example/example-serializer-21.kt).
1129+
> You can get the full code [here](../guide/example/example-serializer-22.kt).
10771130
```text
10781131
{"name":"Kotlin","stableReleaseDate":1455494400000}
10791132
```
@@ -1132,7 +1185,7 @@ fun main() {
11321185
}
11331186
```
11341187

1135-
> You can get the full code [here](../guide/example/example-serializer-22.kt).
1188+
> You can get the full code [here](../guide/example/example-serializer-23.kt).
11361189

11371190
This gets all the `Project` properties serialized:
11381191

@@ -1173,7 +1226,7 @@ fun main() {
11731226
}
11741227
```
11751228

1176-
> You can get the full code [here](../guide/example/example-serializer-23.kt).
1229+
> You can get the full code [here](../guide/example/example-serializer-24.kt).
11771230

11781231
The output is shown below.
11791232

@@ -1203,6 +1256,7 @@ The next chapter covers [Polymorphism](polymorphism.md).
12031256
[Serializable.with]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serializable/with.html
12041257
[SerialName]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serial-name/index.html
12051258
[UseSerializers]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-use-serializers/index.html
1259+
[Polymorphic]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/index.html
12061260
[ContextualSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-contextual-serializer/index.html
12071261
[Contextual]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-contextual/index.html
12081262
[UseContextualSerialization]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-use-contextual-serialization/index.html

guide/example/example-json-29.kt

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,30 @@ package example.exampleJson29
44
import kotlinx.serialization.*
55
import kotlinx.serialization.json.*
66

7-
import kotlinx.serialization.descriptors.*
8-
import kotlinx.serialization.encoding.*
9-
10-
@Serializable(with = ResponseSerializer::class)
11-
sealed class Response<out T> {
12-
data class Ok<out T>(val data: T) : Response<T>()
13-
data class Error(val message: String) : Response<Nothing>()
7+
@Serializable
8+
sealed class Project {
9+
abstract val name: String
1410
}
1511

16-
class ResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Response<T>> {
17-
override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) {
18-
element("Ok", dataSerializer.descriptor)
19-
element("Error", buildClassSerialDescriptor("Error") {
20-
element<String>("message")
21-
})
22-
}
23-
24-
override fun deserialize(decoder: Decoder): Response<T> {
25-
// Decoder -> JsonDecoder
26-
require(decoder is JsonDecoder) // this class can be decoded only by Json
27-
// JsonDecoder -> JsonElement
28-
val element = decoder.decodeJsonElement()
29-
// JsonElement -> value
30-
if (element is JsonObject && "error" in element)
31-
return Response.Error(element["error"]!!.jsonPrimitive.content)
32-
return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element))
33-
}
34-
35-
override fun serialize(encoder: Encoder, value: Response<T>) {
36-
// Encoder -> JsonEncoder
37-
require(encoder is JsonEncoder) // This class can be encoded only by Json
38-
// value -> JsonElement
39-
val element = when (value) {
40-
is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data)
41-
is Response.Error -> buildJsonObject { put("error", value.message) }
12+
@KeepGeneratedSerializer
13+
@Serializable(with = BasicProjectSerializer::class)
14+
@SerialName("basic")
15+
data class BasicProject(override val name: String): Project()
16+
17+
object BasicProjectSerializer : JsonTransformingSerializer<BasicProject>(BasicProject.generatedSerializer()) {
18+
override fun transformDeserialize(element: JsonElement): JsonElement {
19+
val jsonObject = element.jsonObject
20+
return if ("basic-name" in jsonObject) {
21+
val nameElement = jsonObject["basic-name"] ?: throw IllegalStateException()
22+
JsonObject(mapOf("name" to nameElement))
23+
} else {
24+
jsonObject
4225
}
43-
// JsonElement -> JsonEncoder
44-
encoder.encodeJsonElement(element)
4526
}
4627
}
4728

48-
@Serializable
49-
data class Project(val name: String)
5029

5130
fun main() {
52-
val responses = listOf(
53-
Response.Ok(Project("kotlinx.serialization")),
54-
Response.Error("Not found")
55-
)
56-
val string = Json.encodeToString(responses)
57-
println(string)
58-
println(Json.decodeFromString<List<Response<Project>>>(string))
31+
val project = Json.decodeFromString<Project>("""{"type":"basic","basic-name":"example"}""")
32+
println(project)
5933
}

0 commit comments

Comments
 (0)