Update default polymorphic serialization guide section (#1031)
* Update polymorphic guide
* Explain limitations of proposed default serializer
* Add JSON section for maintaining an arbitrary attributes
Fixes #1025
Co-authored-by: Roman Elizarov <elizarov@gmail.com>
diff --git a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt
index 0ef5327..57720c3 100644
--- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt
+++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt
@@ -41,6 +41,8 @@
* type and have a precise serializer, so the default serializer has limited capabilities.
* To have a structural access to the unknown data, it is recommended to use [JsonTransformingSerializer]
* or [JsonContentPolymorphicSerializer] classes.
+ *
+ * Default serializers provider affects only deserialization process.
*/
public fun default(defaultSerializerProvider: (className: String?) -> DeserializationStrategy<out Base>?) {
require(this.defaultSerializerProvider == null) {
diff --git a/docs/json.md b/docs/json.md
index 5e74537..61d6aca 100644
--- a/docs/json.md
+++ b/docs/json.md
@@ -29,6 +29,7 @@
* [Manipulating default values](#manipulating-default-values)
* [Content-based polymorphic deserialization](#content-based-polymorphic-deserialization)
* [Under the hood (experimental)](#under-the-hood-experimental)
+ * [Maintaining custom JSON attributes](#maintaining-custom-json-attributes)
<!--- END -->
@@ -812,6 +813,68 @@
<!--- TEST -->
+### Maintaining custom JSON attributes
+
+A good example of custom JSON-specific serializer would be a deserializer
+that packs all unknown JSON properties into a dedicated field of `JsonObject` type.
+
+Let us add `UnknownProject` – class with basic `name` property and arbitrary details flattened into the same object:
+
+<!--- INCLUDE
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+-->
+
+```kotlin
+data class UnknownProject(val name: String, val details: JsonObject)
+```
+
+However, the default plugin-generated serializer requires details
+to be a separate JSON object and that's not what we want.
+
+To mitigate that, we can write our own serializer that leverages the fact that it can only be used with `Json` format
+
+```kotlin
+object UnknownProjectSerializer : KSerializer<UnknownProject> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
+ element<String>("name")
+ element<JsonElement>("details")
+ }
+
+ override fun deserialize(decoder: Decoder): UnknownProject {
+ // Cast to JSON-specific interface
+ val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
+ // Read the whole content as JSON
+ val json = jsonInput.decodeJsonElement().jsonObject
+ // Extract and remove name property
+ val name = json.getValue("name").jsonPrimitive.content
+ val details = json.toMutableMap()
+ details.remove("name")
+ return UnknownProject(name, JsonObject(details))
+ }
+
+ override fun serialize(encoder: Encoder, value: UnknownProject) {
+ error("Serialization is not supported")
+ }
+}
+```
+
+Now it can be used to read flattened JSON details as `UnknownProject`.
+
+```kotlin
+fun main() {
+ println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
+}
+```
+
+> You can get the full code [here](../guide/example/example-json-18.kt).
+
+```text
+UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
+```
+
+<!--- TEST -->
+
---
The next chapter covers [Alternative and custom formats (experimental)](formats.md).
diff --git a/docs/polymorphism.md b/docs/polymorphism.md
index 7e611cb..01ea1f6 100644
--- a/docs/polymorphism.md
+++ b/docs/polymorphism.md
@@ -855,6 +855,11 @@
<!--- TEST -->
+We used a plugin-generated serializer as a default serializer, implying that
+the structure of the "unknown" data is known in advance. In a real-world API it's rarely the case.
+For that purpose a custom, less-structured serializer is needed. You will see the example of such serializer in the future section
+on [Maintaining custom JSON attributes](json.md#maintaining-custom-json-attributes).
+
---
The next chapter covers [JSON features](json.md).
diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md
index 1c13638..f32658e 100644
--- a/docs/serialization-guide.md
+++ b/docs/serialization-guide.md
@@ -122,6 +122,7 @@
* <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='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 -->
**Chapter 6.** [Alternative and custom formats (experimental)](formats.md)
diff --git a/guide/example/example-json-18.kt b/guide/example/example-json-18.kt
new file mode 100644
index 0000000..325472f
--- /dev/null
+++ b/guide/example/example-json-18.kt
@@ -0,0 +1,37 @@
+// This file was automatically generated from json.md by Knit tool. Do not edit.
+package example.exampleJson18
+
+import kotlinx.serialization.*
+import kotlinx.serialization.json.*
+
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+
+data class UnknownProject(val name: String, val details: JsonObject)
+
+object UnknownProjectSerializer : KSerializer<UnknownProject> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
+ element<String>("name")
+ element<JsonElement>("details")
+ }
+
+ override fun deserialize(decoder: Decoder): UnknownProject {
+ // Cast to JSON-specific interface
+ val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
+ // Read the whole content as JSON
+ val json = jsonInput.decodeJsonElement().jsonObject
+ // Extract and remove name property
+ val name = json.getValue("name").jsonPrimitive.content
+ val details = json.toMutableMap()
+ details.remove("name")
+ return UnknownProject(name, JsonObject(details))
+ }
+
+ override fun serialize(encoder: Encoder, value: UnknownProject) {
+ error("Serialization is not supported")
+ }
+}
+
+fun main() {
+ println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
+}
diff --git a/guide/test/JsonTest.kt b/guide/test/JsonTest.kt
index 896fe74..0fe3a88 100644
--- a/guide/test/JsonTest.kt
+++ b/guide/test/JsonTest.kt
@@ -130,4 +130,11 @@
"[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]"
)
}
+
+ @Test
+ fun testExampleJson18() {
+ captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines(
+ "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})"
+ )
+ }
}