Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added documentation for @KeepGeneratedSerializer feature #2669

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import kotlin.reflect.*
* MyAnotherData.serializer() // <- returns MyAnotherDataCustomSerializer
* ```
*
* To continue generating the implementation of [KSerializer] using the plugin, specify the [KeepGeneratedSerializer] annotation.
* In this case, the serializer will be available via `generatedSerializer()` function, and will also be used in the heirs.
*
* For annotated properties, specifying [with] parameter is mandatory and can be used to override
* serializer on the use-site without affecting the rest of the usages:
* ```
Expand Down Expand Up @@ -64,6 +67,7 @@ import kotlin.reflect.*
*
* @see UseSerializers
* @see Serializer
* @see KeepGeneratedSerializer
*/
@MustBeDocumented
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS, AnnotationTarget.TYPE)
Expand Down Expand Up @@ -330,13 +334,15 @@ public annotation class Polymorphic
*
* Automatically generated serializer is available via `generatedSerializer()` function in companion object of serializable class.
*
* Generated serializers allow to use custom serializers on classes from which other serializable classes are inherited.
* Keeping generated serializers allow to use plugin generated serializer in inheritors even if custom serializer is specified.
*
* Used only with annotation [Serializable] with the specified argument [Serializable.with], e.g. `@Serializable(with=SomeSerializer::class)`.
*
* Used only with the [Serializable] annotation.
* Annotation is not allowed on classes involved in polymorphic serialization:
* interfaces, sealed classes, abstract classes, classes marked by [Polymorphic].
*
* A compiler version `2.0.0` and higher is required.
* A compiler version `2.0.20` and higher is required.
*/
@InternalSerializationApi
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
public annotation class KeepGeneratedSerializer
Expand Down
1 change: 1 addition & 0 deletions docs/serialization-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Once the project is set up, we can start serializing some classes.
* <a name='specifying-serializer-globally-using-typealias'></a>[Specifying serializer globally using typealias](serializers.md#specifying-serializer-globally-using-typealias)
* <a name='custom-serializers-for-a-generic-type'></a>[Custom serializers for a generic type](serializers.md#custom-serializers-for-a-generic-type)
* <a name='format-specific-serializers'></a>[Format-specific serializers](serializers.md#format-specific-serializers)
* <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)
* <a name='contextual-serialization'></a>[Contextual serialization](serializers.md#contextual-serialization)
* <a name='serializers-module'></a>[Serializers module](serializers.md#serializers-module)
* <a name='contextual-serialization-and-generic-classes'></a>[Contextual serialization and generic classes](serializers.md#contextual-serialization-and-generic-classes)
Expand Down
64 changes: 59 additions & 5 deletions docs/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ In this chapter we'll take a look at serializers in more detail, and we'll see h
* [Specifying serializer globally using typealias](#specifying-serializer-globally-using-typealias)
* [Custom serializers for a generic type](#custom-serializers-for-a-generic-type)
* [Format-specific serializers](#format-specific-serializers)
* [Simultaneous use of plugin-generated and custom serializers](#simultaneous-use-of-plugin-generated-and-custom-serializers)
* [Contextual serialization](#contextual-serialization)
* [Serializers module](#serializers-module)
* [Contextual serialization and generic classes](#contextual-serialization-and-generic-classes)
Expand Down Expand Up @@ -810,7 +811,7 @@ fun main() {

<!--- TEST -->

### Specifying serializers for a file
### Specifying serializers for a file

A serializer for a specific type, like `Date`, can be specified for a whole source code file with the file-level
[UseSerializers] annotation at the beginning of the file.
Expand Down Expand Up @@ -970,6 +971,58 @@ features that a serializer implementation would like to take advantage of.

This chapter proceeds with a generic approach to tweaking the serialization strategy based on the context.

## Simultaneous use of plugin-generated and custom serializers
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.

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.

In order for the plugin to continue generating the serializer, you must specify the `@KeepGeneratedSerializer` annotation in the type declaration.
In this case, the serializer will be accessible using the `.generatedSerializer()` function on the class's companion object.

Annotation `@KeepGeneratedSerializer` is not allowed on classes involved in polymorphic serialization: interfaces, sealed classes, abstract classes, classes marked by [Polymorphic].

An example of using two serializers at once:

<!--- INCLUDE
object ColorAsStringSerializer : KSerializer<Color> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: Color) {
val string = value.rgb.toString(16).padStart(6, '0')
encoder.encodeString(string)
}

override fun deserialize(decoder: Decoder): Color {
val string = decoder.decodeString()
return Color(string.toInt(16))
}
}
-->

```kotlin
@KeepGeneratedSerializer
@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)


fun main() {
val green = Color(0x00ff00)
println(Json.encodeToString(green))
println(Json.encodeToString(Color.generatedSerializer(), green))
}
```

> You can get the full code [here](../guide/example/example-serializer-20.kt).

As a result, serialization will occur using custom and plugin-generated serializers:

```text
"00ff00"
{"rgb":65280}
```

<!--- TEST -->

## Contextual serialization

All the previous approaches to specifying custom serialization strategies were _static_, that is
Expand Down Expand Up @@ -1009,7 +1062,7 @@ fun main() {
To actually serialize this class we must provide the corresponding context when calling the `encodeToXxx`/`decodeFromXxx`
functions. Without it we'll get a "Serializer for class 'Date' is not found" exception.

> See [here](../guide/example/example-serializer-20.kt) for an example that produces that exception.
> See [here](../guide/example/example-serializer-21.kt) for an example that produces that exception.

<!--- TEST LINES_START
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Date' is not found.
Expand Down Expand Up @@ -1068,7 +1121,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-serializer-21.kt).
> You can get the full code [here](../guide/example/example-serializer-22.kt).
```text
{"name":"Kotlin","stableReleaseDate":1455494400000}
```
Expand Down Expand Up @@ -1127,7 +1180,7 @@ fun main() {
}
```

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

This gets all the `Project` properties serialized:

Expand Down Expand Up @@ -1168,7 +1221,7 @@ fun main() {
}
```

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

The output is shown below.

Expand Down Expand Up @@ -1198,6 +1251,7 @@ The next chapter covers [Polymorphism](polymorphism.md).
[Serializable.with]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serializable/with.html
[SerialName]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serial-name/index.html
[UseSerializers]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-use-serializers/index.html
[Polymorphic]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/index.html
[ContextualSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-contextual-serializer/index.html
[Contextual]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-contextual/index.html
[UseContextualSerialization]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-use-contextual-serialization/index.html
Expand Down
32 changes: 21 additions & 11 deletions guide/example/example-serializer-20.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,27 @@ import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*

import java.util.Date
import java.text.SimpleDateFormat
object ColorAsStringSerializer : KSerializer<Color> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING)

@Serializable
class ProgrammingLanguage(
val name: String,
@Contextual
val stableReleaseDate: Date
)
override fun serialize(encoder: Encoder, value: Color) {
val string = value.rgb.toString(16).padStart(6, '0')
encoder.encodeString(string)
}

fun main() {
val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
println(Json.encodeToString(data))
override fun deserialize(decoder: Decoder): Color {
val string = decoder.decodeString()
return Color(string.toInt(16))
}
}

@KeepGeneratedSerializer
@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)


fun main() {
val green = Color(0x00ff00)
println(Json.encodeToString(green))
println(Json.encodeToString(Color.generatedSerializer(), green))
}
15 changes: 1 addition & 14 deletions guide/example/example-serializer-21.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,8 @@ import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*

import kotlinx.serialization.modules.*
import java.util.Date
import java.text.SimpleDateFormat

object DateAsLongSerializer : KSerializer<Date> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time)
override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong())
}

@Serializable
class ProgrammingLanguage(
Expand All @@ -23,13 +16,7 @@ class ProgrammingLanguage(
val stableReleaseDate: Date
)

private val module = SerializersModule {
contextual(DateAsLongSerializer)
}

val format = Json { serializersModule = module }

fun main() {
val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
println(format.encodeToString(data))
println(Json.encodeToString(data))
}
31 changes: 24 additions & 7 deletions guide/example/example-serializer-22.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,30 @@ import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*

// NOT @Serializable
class Project(val name: String, val language: String)

@Serializer(forClass = Project::class)
object ProjectSerializer
import kotlinx.serialization.modules.*
import java.util.Date
import java.text.SimpleDateFormat

object DateAsLongSerializer : KSerializer<Date> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time)
override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong())
}

@Serializable
class ProgrammingLanguage(
val name: String,
@Contextual
val stableReleaseDate: Date
)

private val module = SerializersModule {
contextual(DateAsLongSerializer)
}

val format = Json { serializersModule = module }

fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(ProjectSerializer, data))
val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
println(format.encodeToString(data))
}
20 changes: 5 additions & 15 deletions guide/example/example-serializer-23.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,13 @@ import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*

// NOT @Serializable, will use external serializer
class Project(
// val in a primary constructor -- serialized
val name: String
) {
var stars: Int = 0 // property with getter & setter -- serialized

val path: String // getter only -- not serialized
get() = "kotlin/$name"

private var locked: Boolean = false // private, not accessible -- not serialized
}

// NOT @Serializable
class Project(val name: String, val language: String)

@Serializer(forClass = Project::class)
object ProjectSerializer

fun main() {
val data = Project("kotlinx.serialization").apply { stars = 9000 }
println(Json.encodeToString(ProjectSerializer, data))
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(ProjectSerializer, data))
}
20 changes: 14 additions & 6 deletions guide/test/SerializersTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,29 +141,37 @@ class SerializersTest {

@Test
fun testExampleSerializer20() {
captureOutput("ExampleSerializer20") { example.exampleSerializer20.main() }.verifyOutputLinesStart(
"Exception in thread \"main\" kotlinx.serialization.SerializationException: Serializer for class 'Date' is not found.",
"Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied."
captureOutput("ExampleSerializer20") { example.exampleSerializer20.main() }.verifyOutputLines(
"\"00ff00\"",
"{\"rgb\":65280}"
)
}

@Test
fun testExampleSerializer21() {
captureOutput("ExampleSerializer21") { example.exampleSerializer21.main() }.verifyOutputLines(
"{\"name\":\"Kotlin\",\"stableReleaseDate\":1455494400000}"
captureOutput("ExampleSerializer21") { example.exampleSerializer21.main() }.verifyOutputLinesStart(
"Exception in thread \"main\" kotlinx.serialization.SerializationException: Serializer for class 'Date' is not found.",
"Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied."
)
}

@Test
fun testExampleSerializer22() {
captureOutput("ExampleSerializer22") { example.exampleSerializer22.main() }.verifyOutputLines(
"{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
"{\"name\":\"Kotlin\",\"stableReleaseDate\":1455494400000}"
)
}

@Test
fun testExampleSerializer23() {
captureOutput("ExampleSerializer23") { example.exampleSerializer23.main() }.verifyOutputLines(
"{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
)
}

@Test
fun testExampleSerializer24() {
captureOutput("ExampleSerializer24") { example.exampleSerializer24.main() }.verifyOutputLines(
"{\"name\":\"kotlinx.serialization\",\"stars\":9000}"
)
}
Expand Down