Skip to content
Merged
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].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this sentence: @KeepGeneratedSerializer is not allowed with any inheritance, but your sample code uses a class with inherits from an interface. (see other comment)

Copy link
Contributor Author

@shanshin shanshin Aug 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means that you cannot specify the @KeepGeneratedSerializer annotation on the interfaces, sealed/abstract classes, but it's ok to add this annotation add to the heirs

*
* 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
53 changes: 51 additions & 2 deletions docs/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json
* [Array unwrapping](#array-unwrapping)
* [Manipulating default values](#manipulating-default-values)
* [Content-based polymorphic deserialization](#content-based-polymorphic-deserialization)
* [Extending the behavior of the plugin generated serializer](#extending-the-behavior-of-the-plugin-generated-serializer)
* [Under the hood (experimental)](#under-the-hood-experimental)
* [Maintaining custom JSON attributes](#maintaining-custom-json-attributes)

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

<!--- TEST -->

### Extending the behavior of the plugin generated serializer
In some cases, it may be necessary to add additional serialization logic on top of the plugin generated logic.
For example, to add a preliminary modification of JSON elements or to add processing of unknown values of enums.

In this case, you can mark the serializable class with the [`@KeepGeneratedSerializer`][KeepGeneratedSerializer] annotation and get the generated serializer using the `generatedSerializer()` function.

Here is an example of the simultaneous use of [JsonTransformingSerializer] and polymorphism.
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.
```kotlin
@Serializable
sealed class Project {
abstract val name: String
}

@KeepGeneratedSerializer
@Serializable(with = BasicProjectSerializer::class)
@SerialName("basic")
data class BasicProject(override val name: String): Project()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, do you test your sample automatically? If not, can you add this code to your tests in #2758?
Background: I use Kotlin 2.0.20-RC with kx-serial 1.7.1, and I get this error:

java.lang.NullPointerException
	at kotlinx.serialization.SealedClassSerializer$special$$inlined$groupingBy$1.keyOf(_Collections.kt:1547)

I will also create an issue with a small reproducer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, do you test your sample automatically?

yes

I will also create an issue with a small reproducer.

Thanks!


object BasicProjectSerializer : JsonTransformingSerializer<BasicProject>(BasicProject.generatedSerializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val jsonObject = element.jsonObject
return if ("basic-name" in jsonObject) {
val nameElement = jsonObject["basic-name"] ?: throw IllegalStateException()
JsonObject(mapOf("name" to nameElement))
} else {
jsonObject
}
}
}


fun main() {
val project = Json.decodeFromString<Project>("""{"type":"basic","basic-name":"example"}""")
println(project)
}
```

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

`BasicProject` will be printed to the output:

```text
BasicProject(name=example)
```
<!--- TEST -->

### Under the hood (experimental)

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

> You can get the full code [here](../guide/example/example-json-29.kt).
> You can get the full code [here](../guide/example/example-json-30.kt).

This gives you fine-grained control on the representation of the `Response` class in the JSON output:

Expand Down Expand Up @@ -1410,7 +1458,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-json-30.kt).
> You can get the full code [here](../guide/example/example-json-31.kt).

```text
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
Expand Down Expand Up @@ -1440,6 +1488,7 @@ The next chapter covers [Alternative and custom formats (experimental)](formats.
[InheritableSerialInfo]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-inheritable-serial-info/index.html
[KSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/index.html
[Serializable]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serializable/index.html
[KeepGeneratedSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-keep-generated-serializer/index.html

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

Expand Down
2 changes: 2 additions & 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 Expand Up @@ -137,6 +138,7 @@ Once the project is set up, we can start serializing some classes.
* <a name='array-unwrapping'></a>[Array unwrapping](json.md#array-unwrapping)
* <a name='manipulating-default-values'></a>[Manipulating default values](json.md#manipulating-default-values)
* <a name='content-based-polymorphic-deserialization'></a>[Content-based polymorphic deserialization](json.md#content-based-polymorphic-deserialization)
* <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)
* <a name='under-the-hood-experimental'></a>[Under the hood (experimental)](json.md#under-the-hood-experimental)
* <a name='maintaining-custom-json-attributes'></a>[Maintaining custom JSON attributes](json.md#maintaining-custom-json-attributes)
<!--- END -->
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 @@ -975,6 +976,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 @@ -1014,7 +1067,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 @@ -1073,7 +1126,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 @@ -1132,7 +1185,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 @@ -1173,7 +1226,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 @@ -1203,6 +1256,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
62 changes: 18 additions & 44 deletions guide/example/example-json-29.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,30 @@ package example.exampleJson29
import kotlinx.serialization.*
import kotlinx.serialization.json.*

import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

@Serializable(with = ResponseSerializer::class)
sealed class Response<out T> {
data class Ok<out T>(val data: T) : Response<T>()
data class Error(val message: String) : Response<Nothing>()
@Serializable
sealed class Project {
abstract val name: String
}

class ResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Response<T>> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) {
element("Ok", dataSerializer.descriptor)
element("Error", buildClassSerialDescriptor("Error") {
element<String>("message")
})
}

override fun deserialize(decoder: Decoder): Response<T> {
// Decoder -> JsonDecoder
require(decoder is JsonDecoder) // this class can be decoded only by Json
// JsonDecoder -> JsonElement
val element = decoder.decodeJsonElement()
// JsonElement -> value
if (element is JsonObject && "error" in element)
return Response.Error(element["error"]!!.jsonPrimitive.content)
return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element))
}

override fun serialize(encoder: Encoder, value: Response<T>) {
// Encoder -> JsonEncoder
require(encoder is JsonEncoder) // This class can be encoded only by Json
// value -> JsonElement
val element = when (value) {
is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data)
is Response.Error -> buildJsonObject { put("error", value.message) }
@KeepGeneratedSerializer
@Serializable(with = BasicProjectSerializer::class)
@SerialName("basic")
data class BasicProject(override val name: String): Project()

object BasicProjectSerializer : JsonTransformingSerializer<BasicProject>(BasicProject.generatedSerializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val jsonObject = element.jsonObject
return if ("basic-name" in jsonObject) {
val nameElement = jsonObject["basic-name"] ?: throw IllegalStateException()
JsonObject(mapOf("name" to nameElement))
} else {
jsonObject
}
// JsonElement -> JsonEncoder
encoder.encodeJsonElement(element)
}
}

@Serializable
data class Project(val name: String)

fun main() {
val responses = listOf(
Response.Ok(Project("kotlinx.serialization")),
Response.Error("Not found")
)
val string = Json.encodeToString(responses)
println(string)
println(Json.decodeFromString<List<Response<Project>>>(string))
val project = Json.decodeFromString<Project>("""{"type":"basic","basic-name":"example"}""")
println(project)
}
Loading