Implement ClassDiscriminatorMode.ALL, .NONE, and .POLYMORPHIC (#2532)

Implement ClassDiscriminatorMode.ALL, .NONE, and .POLYMORPHIC

As a part of the solution for #1247 
diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt
index f01f952..8a9126d 100644
--- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt
+++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt
@@ -8,7 +8,6 @@
 import kotlinx.serialization.internal.*
 import kotlin.js.*
 import kotlin.jvm.*
-import kotlin.native.concurrent.*
 import kotlin.reflect.*
 
 /**
diff --git a/docs/json.md b/docs/json.md
index d764ce5..eaa2b79 100644
--- a/docs/json.md
+++ b/docs/json.md
@@ -20,6 +20,7 @@
   * [Allowing structured map keys](#allowing-structured-map-keys)
   * [Allowing special floating-point values](#allowing-special-floating-point-values)
   * [Class discriminator for polymorphism](#class-discriminator-for-polymorphism)
+  * [Class discriminator output mode](#class-discriminator-output-mode)
   * [Decoding enums in a case-insensitive manner](#decoding-enums-in-a-case-insensitive-manner)
   * [Global naming strategy](#global-naming-strategy)
 * [Json elements](#json-elements)
@@ -470,6 +471,45 @@
 
 <!--- TEST -->
 
+### Class discriminator output mode
+
+Class discriminator provides information for serializing and deserializing [polymorphic class hierarchies](polymorphism.md#sealed-classes).
+As shown above, it is only added for polymorphic classes by default.
+In case you want to encode more or less information for various third party APIs about types in the output, it is possible to control
+addition of the class discriminator with the [JsonBuilder.classDiscriminatorMode] property.
+
+For example, [ClassDiscriminatorMode.NONE] does not add class discriminator at all, in case the receiving party is not interested in Kotlin types:
+
+```kotlin
+val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE }
+
+@Serializable
+sealed class Project {
+    abstract val name: String
+}
+
+@Serializable
+class OwnedProject(override val name: String, val owner: String) : Project()
+
+fun main() {
+    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
+    println(format.encodeToString(data))
+}
+```
+
+> You can get the full code [here](../guide/example/example-json-12.kt).
+
+Note that it would be impossible to deserialize this output back with kotlinx.serialization.
+
+```text
+{"name":"kotlinx.coroutines","owner":"kotlin"}
+```
+
+Two other available values are [ClassDiscriminatorMode.POLYMORPHIC] (default behavior) and [ClassDiscriminatorMode.ALL_JSON_OBJECTS] (adds discriminator whenever possible).
+Consult their documentation for details.
+
+<!--- TEST -->
+
 ### Decoding enums in a case-insensitive manner
 
 [Kotlin's naming policy recommends](https://kotlinlang.org/docs/coding-conventions.html#property-names) naming enum values
@@ -491,7 +531,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-12.kt).
+> You can get the full code [here](../guide/example/example-json-13.kt).
 
 It affects serial names as well as alternative names specified with [JsonNames] annotation, so both values are successfully decoded:
 
@@ -523,7 +563,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-13.kt).
+> You can get the full code [here](../guide/example/example-json-14.kt).
 
 As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case:
 
@@ -575,7 +615,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-14.kt).
+> You can get the full code [here](../guide/example/example-json-15.kt).
 
 A `JsonElement` prints itself as a valid JSON:
 
@@ -618,7 +658,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-15.kt).
+> You can get the full code [here](../guide/example/example-json-16.kt).
 
 The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`:
 
@@ -658,7 +698,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-16.kt).
+> You can get the full code [here](../guide/example/example-json-17.kt).
 
 As a result, you get a proper JSON string:
 
@@ -687,7 +727,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-17.kt).
+> You can get the full code [here](../guide/example/example-json-18.kt).
 
 The result is exactly what you would expect:
 
@@ -733,7 +773,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-18.kt).
+> You can get the full code [here](../guide/example/example-json-19.kt).
 
 Even though `pi` was defined as a number with 30 decimal places, the resulting JSON does not reflect this. 
 The [Double] value is truncated to 15 decimal places, and the String is wrapped in quotes - which is not a JSON number.
@@ -773,7 +813,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-19.kt).
+> You can get the full code [here](../guide/example/example-json-20.kt).
 
 `pi_literal` now accurately matches the value defined.
 
@@ -813,7 +853,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-20.kt).
+> You can get the full code [here](../guide/example/example-json-21.kt).
 
 The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON.
 
@@ -835,7 +875,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-21.kt).
+> You can get the full code [here](../guide/example/example-json-22.kt).
 
 ```text
 Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive
@@ -911,7 +951,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-22.kt).
+> You can get the full code [here](../guide/example/example-json-23.kt).
 
 The output shows that both cases are correctly deserialized into a Kotlin [List].
 
@@ -963,7 +1003,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-23.kt).
+> You can get the full code [here](../guide/example/example-json-24.kt).
 
 You end up with a single JSON object, not an array with one element:
 
@@ -1008,7 +1048,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-24.kt).
+> You can get the full code [here](../guide/example/example-json-25.kt).
 
 See the effect of the custom serializer:
 
@@ -1081,7 +1121,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-25.kt).
+> You can get the full code [here](../guide/example/example-json-26.kt).
 
 No class discriminator is added in the JSON output:
 
@@ -1177,7 +1217,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-26.kt).
+> You can get the full code [here](../guide/example/example-json-27.kt).
 
 This gives you fine-grained control on the representation of the `Response` class in the JSON output:
 
@@ -1242,7 +1282,7 @@
 }
 ```
 
-> You can get the full code [here](../guide/example/example-json-27.kt).
+> You can get the full code [here](../guide/example/example-json-28.kt).
 
 ```text
 UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
@@ -1296,6 +1336,10 @@
 [JsonBuilder.allowSpecialFloatingPointValues]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/allow-special-floating-point-values.html
 [JsonBuilder.classDiscriminator]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/class-discriminator.html
 [JsonClassDiscriminator]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-class-discriminator/index.html
+[JsonBuilder.classDiscriminatorMode]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/class-discriminator-mode.html
+[ClassDiscriminatorMode.NONE]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-class-discriminator-mode/-n-o-n-e/index.html
+[ClassDiscriminatorMode.POLYMORPHIC]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-class-discriminator-mode/-p-o-l-y-m-o-r-p-h-i-c/index.html
+[ClassDiscriminatorMode.ALL_JSON_OBJECTS]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-class-discriminator-mode/-a-l-l_-j-s-o-n_-o-b-j-e-c-t-s/index.html
 [JsonBuilder.decodeEnumsCaseInsensitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/decode-enums-case-insensitive.html
 [JsonBuilder.namingStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/naming-strategy.html
 [JsonNamingStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-naming-strategy/index.html
diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md
index 68ede14..50cb130 100644
--- a/docs/serialization-guide.md
+++ b/docs/serialization-guide.md
@@ -120,6 +120,7 @@
   * <a name='allowing-structured-map-keys'></a>[Allowing structured map keys](json.md#allowing-structured-map-keys)
   * <a name='allowing-special-floating-point-values'></a>[Allowing special floating-point values](json.md#allowing-special-floating-point-values)
   * <a name='class-discriminator-for-polymorphism'></a>[Class discriminator for polymorphism](json.md#class-discriminator-for-polymorphism)
+  * <a name='class-discriminator-output-mode'></a>[Class discriminator output mode](json.md#class-discriminator-output-mode)
   * <a name='decoding-enums-in-a-case-insensitive-manner'></a>[Decoding enums in a case-insensitive manner](json.md#decoding-enums-in-a-case-insensitive-manner)
   * <a name='global-naming-strategy'></a>[Global naming strategy](json.md#global-naming-strategy)
 * <a name='json-elements'></a>[Json elements](json.md#json-elements)
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeBaseTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeBaseTest.kt
new file mode 100644
index 0000000..8fcd549
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeBaseTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.json.polymorphic
+
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+import kotlinx.serialization.json.*
+import kotlinx.serialization.modules.*
+import kotlin.test.*
+
+abstract class JsonClassDiscriminatorModeBaseTest(
+    val discriminator: ClassDiscriminatorMode,
+    val deserializeBack: Boolean = true
+) : JsonTestBase() {
+
+    @Serializable
+    sealed class SealedBase
+
+    @Serializable
+    @SerialName("container")
+    data class SealedContainer(val i: Inner): SealedBase()
+
+    @Serializable
+    @SerialName("inner")
+    data class Inner(val x: String, val e: SampleEnum = SampleEnum.OptionB)
+
+    @Serializable
+    @SerialName("outer")
+    data class Outer(val inn: Inner, val lst: List<Inner>, val lss: List<String>)
+
+    data class ContextualType(val text: String)
+
+    object CtxSerializer : KSerializer<ContextualType> {
+        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CtxSerializer") {
+            element("a", String.serializer().descriptor)
+            element("b", String.serializer().descriptor)
+        }
+
+        override fun serialize(encoder: Encoder, value: ContextualType) {
+            encoder.encodeStructure(descriptor) {
+                encodeStringElement(descriptor, 0, value.text.substringBefore("#"))
+                encodeStringElement(descriptor, 1, value.text.substringAfter("#"))
+            }
+        }
+
+        override fun deserialize(decoder: Decoder): ContextualType {
+            lateinit var a: String
+            lateinit var b: String
+            decoder.decodeStructure(descriptor) {
+                while (true) {
+                    when (decodeElementIndex(descriptor)) {
+                        0 -> a = decodeStringElement(descriptor, 0)
+                        1 -> b = decodeStringElement(descriptor, 1)
+                        else -> break
+                    }
+                }
+            }
+            return ContextualType("$a#$b")
+        }
+    }
+
+    @Serializable
+    @SerialName("withContextual")
+    data class WithContextual(@Contextual val ctx: ContextualType, val i: Inner)
+
+    val ctxModule = serializersModuleOf(CtxSerializer)
+
+    val json = Json(default) {
+        ignoreUnknownKeys = true
+        serializersModule = polymorphicTestModule + ctxModule
+        encodeDefaults = true
+        classDiscriminatorMode = discriminator
+    }
+
+    @Serializable
+    @SerialName("mixed")
+    data class MixedPolyAndRegular(val sb: SealedBase, val sc: SealedContainer, val i: Inner)
+
+    private inline fun <reified T> doTest(expected: String, obj: T) {
+        parametrizedTest { mode ->
+            val serialized = json.encodeToString(serializer<T>(), obj, mode)
+            assertEquals(expected, serialized, "Failed with mode = $mode")
+            if (deserializeBack) {
+                val deserialized: T = json.decodeFromString(serializer(), serialized, mode)
+                assertEquals(obj, deserialized, "Failed with mode = $mode")
+            }
+        }
+    }
+
+    fun testMixed(expected: String) {
+        val i = Inner("in", SampleEnum.OptionC)
+        val o = MixedPolyAndRegular(SealedContainer(i), SealedContainer(i), i)
+        doTest(expected, o)
+    }
+
+    fun testIncludeNonPolymorphic(expected: String) {
+        val o = Outer(Inner("X"), listOf(Inner("a"), Inner("b")), listOf("foo"))
+        doTest(expected, o)
+    }
+
+    fun testIncludePolymorphic(expected: String) {
+        val o = OuterNullableBox(OuterNullableImpl(InnerImpl(42), null), InnerImpl2(239))
+        doTest(expected, o)
+    }
+
+    fun testIncludeSealed(expected: String) {
+        val b = Box<SealedBase>(SealedContainer(Inner("x", SampleEnum.OptionC)))
+        doTest(expected, b)
+    }
+
+    fun testContextual(expected: String) {
+        val c = WithContextual(ContextualType("c#d"), Inner("x"))
+        doTest(expected, c)
+    }
+
+    @Serializable
+    @JsonClassDiscriminator("message_type")
+    sealed class Base
+
+    @Serializable // Class discriminator is inherited from Base
+    sealed class ErrorClass : Base()
+
+    @Serializable
+    @SerialName("ErrorClassImpl")
+    data class ErrorClassImpl(val msg: String) : ErrorClass()
+
+    @Serializable
+    @SerialName("Cont")
+    data class Cont(val ec: ErrorClass, val eci: ErrorClassImpl)
+
+    fun testCustomDiscriminator(expected: String) {
+        val c = Cont(ErrorClassImpl("a"), ErrorClassImpl("b"))
+        doTest(expected, c)
+    }
+
+    fun testTopLevelPolyImpl(expectedOpen: String, expectedSealed: String) {
+        assertEquals(expectedOpen, json.encodeToString(InnerImpl(42)))
+        assertEquals(expectedSealed, json.encodeToString(SealedContainer(Inner("x"))))
+    }
+
+    @Serializable
+    @SerialName("NullableMixed")
+    data class NullableMixed(val sb: SealedBase?, val sc: SealedContainer?)
+
+    fun testNullable(expected: String) {
+        val nm = NullableMixed(null, null)
+       doTest(expected, nm)
+    }
+}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt
new file mode 100644
index 0000000..b2f4713
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.json.polymorphic
+
+import kotlinx.serialization.json.*
+import kotlin.test.*
+
+class ClassDiscriminatorModeAllObjectsTest :
+    JsonClassDiscriminatorModeBaseTest(ClassDiscriminatorMode.ALL_JSON_OBJECTS) {
+    @Test
+    fun testIncludeNonPolymorphic() = testIncludeNonPolymorphic("""{"type":"outer","inn":{"type":"inner","x":"X","e":"OptionB"},"lst":[{"type":"inner","x":"a","e":"OptionB"},{"type":"inner","x":"b","e":"OptionB"}],"lss":["foo"]}""")
+
+    @Test
+    fun testIncludePolymorphic() {
+        val s = """{"type":"kotlinx.serialization.json.polymorphic.OuterNullableBox","outerBase":{"type":"kotlinx.serialization.json.polymorphic.OuterNullableImpl","""+
+            """"base":{"type":"kotlinx.serialization.json.polymorphic.InnerImpl","field":42,"str":"default","nullable":null},"base2":null},"innerBase":{"type":"kotlinx.serialization.json.polymorphic.InnerImpl2","field":239}}"""
+        testIncludePolymorphic(s)
+    }
+
+    @Test
+    fun testIncludeSealed() {
+        testIncludeSealed("""{"type":"kotlinx.serialization.Box","boxed":{"type":"container","i":{"type":"inner","x":"x","e":"OptionC"}}}""")
+    }
+
+    @Test
+    fun testIncludeMixed() = testMixed("""{"type":"mixed","sb":{"type":"container","i":{"type":"inner","x":"in","e":"OptionC"}},"sc":{"type":"container","i":{"type":"inner","x":"in","e":"OptionC"}},"i":{"type":"inner","x":"in","e":"OptionC"}}""")
+
+    @Test
+    fun testIncludeCtx() =
+        testContextual("""{"type":"withContextual","ctx":{"type":"CtxSerializer","a":"c","b":"d"},"i":{"type":"inner","x":"x","e":"OptionB"}}""")
+
+    @Test
+    fun testIncludeCustomDiscriminator() =
+        testCustomDiscriminator("""{"type":"Cont","ec":{"message_type":"ErrorClassImpl","msg":"a"},"eci":{"message_type":"ErrorClassImpl","msg":"b"}}""")
+
+    @Test
+    fun testTopLevelPolyImpl() = testTopLevelPolyImpl(
+        """{"type":"kotlinx.serialization.json.polymorphic.InnerImpl","field":42,"str":"default","nullable":null}""",
+        """{"type":"container","i":{"type":"inner","x":"x","e":"OptionB"}}"""
+    )
+
+    @Test
+    fun testNullable() = testNullable("""{"type":"NullableMixed","sb":null,"sc":null}""")
+
+}
+
+class ClassDiscriminatorModeNoneTest :
+    JsonClassDiscriminatorModeBaseTest(ClassDiscriminatorMode.NONE, deserializeBack = false) {
+    @Test
+    fun testIncludeNonPolymorphic() = testIncludeNonPolymorphic("""{"inn":{"x":"X","e":"OptionB"},"lst":[{"x":"a","e":"OptionB"},{"x":"b","e":"OptionB"}],"lss":["foo"]}""")
+
+    @Test
+    fun testIncludePolymorphic() {
+        val s = """{"outerBase":{"base":{"field":42,"str":"default","nullable":null},"base2":null},"innerBase":{"field":239}}"""
+        testIncludePolymorphic(s)
+    }
+
+    @Test
+    fun testIncludeSealed() {
+        testIncludeSealed("""{"boxed":{"i":{"x":"x","e":"OptionC"}}}""")
+    }
+
+    @Test
+    fun testIncludeMixed() = testMixed("""{"sb":{"i":{"x":"in","e":"OptionC"}},"sc":{"i":{"x":"in","e":"OptionC"}},"i":{"x":"in","e":"OptionC"}}""")
+
+    @Test
+    fun testIncludeCtx() =
+        testContextual("""{"ctx":{"a":"c","b":"d"},"i":{"x":"x","e":"OptionB"}}""")
+
+    @Test
+    fun testIncludeCustomDiscriminator() = testCustomDiscriminator("""{"ec":{"msg":"a"},"eci":{"msg":"b"}}""")
+
+    @Test
+    fun testTopLevelPolyImpl() = testTopLevelPolyImpl(
+        """{"field":42,"str":"default","nullable":null}""",
+        """{"i":{"x":"x","e":"OptionB"}}"""
+    )
+
+    @Test
+    fun testNullable() = testNullable("""{"sb":null,"sc":null}""")
+}
+
diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api
index 649cce0..88dd29c 100644
--- a/formats/json/api/kotlinx-serialization-json.api
+++ b/formats/json/api/kotlinx-serialization-json.api
@@ -1,3 +1,12 @@
+public final class kotlinx/serialization/json/ClassDiscriminatorMode : java/lang/Enum {
+	public static final field ALL_JSON_OBJECTS Lkotlinx/serialization/json/ClassDiscriminatorMode;
+	public static final field NONE Lkotlinx/serialization/json/ClassDiscriminatorMode;
+	public static final field POLYMORPHIC Lkotlinx/serialization/json/ClassDiscriminatorMode;
+	public static fun getEntries ()Lkotlin/enums/EnumEntries;
+	public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/json/ClassDiscriminatorMode;
+	public static fun values ()[Lkotlinx/serialization/json/ClassDiscriminatorMode;
+}
+
 public final class kotlinx/serialization/json/DecodeSequenceMode : java/lang/Enum {
 	public static final field ARRAY_WRAPPED Lkotlinx/serialization/json/DecodeSequenceMode;
 	public static final field AUTO_DETECT Lkotlinx/serialization/json/DecodeSequenceMode;
@@ -89,6 +98,7 @@
 	public final fun getAllowStructuredMapKeys ()Z
 	public final fun getAllowTrailingComma ()Z
 	public final fun getClassDiscriminator ()Ljava/lang/String;
+	public final fun getClassDiscriminatorMode ()Lkotlinx/serialization/json/ClassDiscriminatorMode;
 	public final fun getCoerceInputValues ()Z
 	public final fun getDecodeEnumsCaseInsensitive ()Z
 	public final fun getEncodeDefaults ()Z
@@ -105,6 +115,7 @@
 	public final fun setAllowStructuredMapKeys (Z)V
 	public final fun setAllowTrailingComma (Z)V
 	public final fun setClassDiscriminator (Ljava/lang/String;)V
+	public final fun setClassDiscriminatorMode (Lkotlinx/serialization/json/ClassDiscriminatorMode;)V
 	public final fun setCoerceInputValues (Z)V
 	public final fun setDecodeEnumsCaseInsensitive (Z)V
 	public final fun setEncodeDefaults (Z)V
@@ -134,6 +145,7 @@
 	public final fun getAllowStructuredMapKeys ()Z
 	public final fun getAllowTrailingComma ()Z
 	public final fun getClassDiscriminator ()Ljava/lang/String;
+	public final fun getClassDiscriminatorMode ()Lkotlinx/serialization/json/ClassDiscriminatorMode;
 	public final fun getCoerceInputValues ()Z
 	public final fun getDecodeEnumsCaseInsensitive ()Z
 	public final fun getEncodeDefaults ()Z
@@ -145,6 +157,7 @@
 	public final fun getUseAlternativeNames ()Z
 	public final fun getUseArrayPolymorphism ()Z
 	public final fun isLenient ()Z
+	public final fun setClassDiscriminatorMode (Lkotlinx/serialization/json/ClassDiscriminatorMode;)V
 	public fun toString ()Ljava/lang/String;
 }
 
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
index a510e8a..e09b9ed 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
@@ -299,6 +299,8 @@
      * Switches polymorphic serialization to the default array format.
      * This is an option for legacy JSON format and should not be generally used.
      * `false` by default.
+     *
+     * This option can only be used if [classDiscriminatorMode] in a default [ClassDiscriminatorMode.POLYMORPHIC] state.
      */
     public var useArrayPolymorphism: Boolean = json.configuration.useArrayPolymorphism
 
@@ -308,6 +310,16 @@
      */
     public var classDiscriminator: String = json.configuration.classDiscriminator
 
+
+    /**
+     * Defines which classes and objects should have class discriminator added to the output.
+     * [ClassDiscriminatorMode.POLYMORPHIC] by default.
+     *
+     * Other modes are generally intended to produce JSON for consumption by third-party libraries,
+     * therefore, this setting does not affect the deserialization process.
+     */
+    public var classDiscriminatorMode: ClassDiscriminatorMode = json.configuration.classDiscriminatorMode
+
     /**
      * Removes JSON specification restriction on
      * special floating-point values such as `NaN` and `Infinity` and enables their serialization and deserialization.
@@ -385,8 +397,13 @@
 
     @OptIn(ExperimentalSerializationApi::class)
     internal fun build(): JsonConfiguration {
-        if (useArrayPolymorphism) require(classDiscriminator == defaultDiscriminator) {
-            "Class discriminator should not be specified when array polymorphism is specified"
+        if (useArrayPolymorphism) {
+            require(classDiscriminator == defaultDiscriminator) {
+                "Class discriminator should not be specified when array polymorphism is specified"
+            }
+            require(classDiscriminatorMode == ClassDiscriminatorMode.POLYMORPHIC) {
+                "useArrayPolymorphism option can only be used if classDiscriminatorMode in a default POLYMORPHIC state."
+            }
         }
 
         if (!prettyPrint) {
@@ -406,7 +423,7 @@
             allowStructuredMapKeys, prettyPrint, explicitNulls, prettyPrintIndent,
             coerceInputValues, useArrayPolymorphism,
             classDiscriminator, allowSpecialFloatingPointValues, useAlternativeNames,
-            namingStrategy, decodeEnumsCaseInsensitive, allowTrailingComma
+            namingStrategy, decodeEnumsCaseInsensitive, allowTrailingComma, classDiscriminatorMode
         )
     }
 }
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
index 053f4cd..1fa1644 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
@@ -1,6 +1,8 @@
 package kotlinx.serialization.json
 
 import kotlinx.serialization.*
+import kotlinx.serialization.modules.*
+import kotlinx.serialization.descriptors.*
 
 /**
  * Configuration of the current [Json] instance available through [Json.configuration]
@@ -35,6 +37,8 @@
     public val decodeEnumsCaseInsensitive: Boolean = false,
     @ExperimentalSerializationApi
     public val allowTrailingComma: Boolean = false,
+    @ExperimentalSerializationApi
+    public var classDiscriminatorMode: ClassDiscriminatorMode = ClassDiscriminatorMode.POLYMORPHIC,
 ) {
 
     /** @suppress Dokka **/
@@ -43,7 +47,88 @@
         return "JsonConfiguration(encodeDefaults=$encodeDefaults, ignoreUnknownKeys=$ignoreUnknownKeys, isLenient=$isLenient, " +
                 "allowStructuredMapKeys=$allowStructuredMapKeys, prettyPrint=$prettyPrint, explicitNulls=$explicitNulls, " +
                 "prettyPrintIndent='$prettyPrintIndent', coerceInputValues=$coerceInputValues, useArrayPolymorphism=$useArrayPolymorphism, " +
-                "classDiscriminator='$classDiscriminator', allowSpecialFloatingPointValues=$allowSpecialFloatingPointValues, useAlternativeNames=$useAlternativeNames, " +
-                "namingStrategy=$namingStrategy, decodeEnumsCaseInsensitive=$decodeEnumsCaseInsensitive, allowTrailingComma=$allowTrailingComma)"
+                "classDiscriminator='$classDiscriminator', allowSpecialFloatingPointValues=$allowSpecialFloatingPointValues, " +
+                "useAlternativeNames=$useAlternativeNames, namingStrategy=$namingStrategy, decodeEnumsCaseInsensitive=$decodeEnumsCaseInsensitive, " +
+                "allowTrailingComma=$allowTrailingComma, classDiscriminatorMode=$classDiscriminatorMode)"
     }
 }
+
+/**
+ * Defines which classes and objects should have their serial name included in the json as so-called class discriminator.
+ *
+ * Class discriminator is a JSON field added by kotlinx.serialization that has [JsonBuilder.classDiscriminator] as a key (`type` by default),
+ * and class' serial name as a value (fully-qualified name by default, can be changed with [SerialName] annotation).
+ *
+ * Class discriminator is important for serializing and deserializing [polymorphic class hierarchies](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes).
+ * Default [ClassDiscriminatorMode.POLYMORPHIC] mode adds discriminator only to polymorphic classes.
+ * This behavior can be changed to match various JSON schemas.
+ *
+ * @see JsonBuilder.classDiscriminator
+ * @see JsonBuilder.classDiscriminatorMode
+ * @see Polymorphic
+ * @see PolymorphicSerializer
+ */
+public enum class ClassDiscriminatorMode {
+    /**
+     * Never include class discriminator in the output.
+     *
+     * This mode is generally intended to produce JSON for consumption by third-party libraries.
+     * kotlinx.serialization is unable to deserialize [polymorphic classes][POLYMORPHIC] without class discriminators,
+     * so it is impossible to deserialize JSON produced in this mode if a data model has polymorphic classes.
+     */
+    NONE,
+
+    /**
+     * Include class discriminators whenever possible.
+     *
+     * Given that class discriminator is added as a JSON field, adding class discriminator is possible
+     * when the resulting JSON is a json object — i.e., for Kotlin classes, `object`s, and interfaces.
+     * More specifically, discriminator is added to the output of serializers which descriptors
+     * have a [kind][SerialDescriptor.kind] of either [StructureKind.CLASS] or [StructureKind.OBJECT].
+     *
+     * This mode is generally intended to produce JSON for consumption by third-party libraries.
+     * Given that [JsonBuilder.classDiscriminatorMode] does not affect deserialization, kotlinx.serialization
+     * does not expect every object to have discriminator, which may trigger deserialization errors.
+     * If you experience such problems, refrain from using [ALL_JSON_OBJECTS] or use [JsonBuilder.ignoreUnknownKeys].
+     *
+     * In the example:
+     * ```
+     * @Serializable class Plain(val p: String)
+     * @Serializable sealed class Base
+     * @Serializable object Impl: Base()
+     *
+     * @Serializable class All(val p: Plain, val b: Base, val i: Impl)
+     * ```
+     * setting [JsonBuilder.classDiscriminatorMode] to [ClassDiscriminatorMode.ALL_JSON_OBJECTS] adds
+     * class discriminator to `All.p`, `All.b`, `All.i`, and to `All` object itself.
+     */
+    ALL_JSON_OBJECTS,
+
+    /**
+     * Include class discriminators for polymorphic classes.
+     *
+     * Sealed classes, abstract classes, and interfaces are polymorphic classes by definition.
+     * Open classes can be polymorphic if they are serializable with [PolymorphicSerializer]
+     * and properly registered in the [SerializersModule].
+     * See [kotlinx.serialization polymorphism guide](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes) for details.
+     *
+     * Note that implementations of polymorphic classes (e.g., sealed class inheritors) are not polymorphic classes from kotlinx.serialization standpoint.
+     * This means that this mode adds class discriminators only if a statically known type of the property is a base class or interface.
+     *
+     * In the example:
+     * ```
+     * @Serializable class Plain(val p: String)
+     * @Serializable sealed class Base
+     * @Serializable object Impl: Base()
+     *
+     * @Serializable class All(val p: Plain, val b: Base, val i: Impl)
+     * ```
+     * setting [JsonBuilder.classDiscriminatorMode] to [ClassDiscriminatorMode.POLYMORPHIC] adds
+     * class discriminator to `All.b`, but leaves `All.p` and `All.i` intact.
+     *
+     * @see SerializersModule
+     * @see SerializersModuleBuilder
+     * @see PolymorphicModuleBuilder
+     */
+    POLYMORPHIC,
+}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt
deleted file mode 100644
index e69de29..0000000
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt
+++ /dev/null
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
index bd658fc..636f340 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
@@ -9,6 +9,7 @@
 import kotlinx.serialization.descriptors.*
 import kotlinx.serialization.internal.*
 import kotlinx.serialization.json.*
+import kotlinx.serialization.modules.*
 import kotlin.jvm.*
 
 @Suppress("UNCHECKED_CAST")
@@ -17,22 +18,37 @@
     value: T,
     ifPolymorphic: (String) -> Unit
 ) {
-    if (serializer !is AbstractPolymorphicSerializer<*> || json.configuration.useArrayPolymorphism) {
+    if (json.configuration.useArrayPolymorphism) {
         serializer.serialize(this, value)
         return
     }
-    val casted = serializer as AbstractPolymorphicSerializer<Any>
-    val baseClassDiscriminator = serializer.descriptor.classDiscriminator(json)
-    val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
-    validateIfSealed(casted, actualSerializer, baseClassDiscriminator)
-    checkKind(actualSerializer.descriptor.kind)
-    ifPolymorphic(baseClassDiscriminator)
+    val isPolymorphicSerializer = serializer is AbstractPolymorphicSerializer<*>
+    val needDiscriminator =
+        if (isPolymorphicSerializer) {
+            json.configuration.classDiscriminatorMode != ClassDiscriminatorMode.NONE
+        } else {
+            when (json.configuration.classDiscriminatorMode) {
+                ClassDiscriminatorMode.NONE, ClassDiscriminatorMode.POLYMORPHIC /* already handled in isPolymorphicSerializer */ -> false
+                ClassDiscriminatorMode.ALL_JSON_OBJECTS -> serializer.descriptor.kind.let { it == StructureKind.CLASS || it == StructureKind.OBJECT }
+            }
+        }
+    val baseClassDiscriminator = if (needDiscriminator) serializer.descriptor.classDiscriminator(json) else null
+    val actualSerializer: SerializationStrategy<T> = if (isPolymorphicSerializer) {
+        val casted = serializer as AbstractPolymorphicSerializer<Any>
+        requireNotNull(value) { "Value for serializer ${serializer.descriptor} should always be non-null. Please report issue to the kotlinx.serialization tracker." }
+        val actual = casted.findPolymorphicSerializer(this, value)
+        if (baseClassDiscriminator != null) validateIfSealed(serializer, actual, baseClassDiscriminator)
+        checkKind(actual.descriptor.kind)
+        actual as SerializationStrategy<T>
+    } else serializer
+
+    if (baseClassDiscriminator != null) ifPolymorphic(baseClassDiscriminator)
     actualSerializer.serialize(this, value)
 }
 
 private fun validateIfSealed(
     serializer: SerializationStrategy<*>,
-    actualSerializer: SerializationStrategy<Any>,
+    actualSerializer: SerializationStrategy<*>,
     classDiscriminator: String
 ) {
     if (serializer !is SealedClassSerializer<*>) return
diff --git a/guide/example/example-json-12.kt b/guide/example/example-json-12.kt
index 1a37516..99a872b 100644
--- a/guide/example/example-json-12.kt
+++ b/guide/example/example-json-12.kt
@@ -4,13 +4,17 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-val format = Json { decodeEnumsCaseInsensitive = true }
-
-enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }
+val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE }
 
 @Serializable
-data class CasesList(val cases: List<Cases>)
+sealed class Project {
+    abstract val name: String
+}
+
+@Serializable
+class OwnedProject(override val name: String, val owner: String) : Project()
 
 fun main() {
-  println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}""")) 
+    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
+    println(format.encodeToString(data))
 }
diff --git a/guide/example/example-json-13.kt b/guide/example/example-json-13.kt
index cd7cf7f..e20afe2 100644
--- a/guide/example/example-json-13.kt
+++ b/guide/example/example-json-13.kt
@@ -4,12 +4,13 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-@Serializable
-data class Project(val projectName: String, val projectOwner: String)
+val format = Json { decodeEnumsCaseInsensitive = true }
 
-val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
+enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }
+
+@Serializable
+data class CasesList(val cases: List<Cases>)
 
 fun main() {
-    val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
-    println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
+  println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}""")) 
 }
diff --git a/guide/example/example-json-14.kt b/guide/example/example-json-14.kt
index 98464dc..50de55f 100644
--- a/guide/example/example-json-14.kt
+++ b/guide/example/example-json-14.kt
@@ -4,9 +4,12 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
+@Serializable
+data class Project(val projectName: String, val projectOwner: String)
+
+val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
+
 fun main() {
-    val element = Json.parseToJsonElement("""
-        {"name":"kotlinx.serialization","language":"Kotlin"}
-    """)
-    println(element)
+    val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
+    println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
 }
diff --git a/guide/example/example-json-15.kt b/guide/example/example-json-15.kt
index 72fd23e..384ae41 100644
--- a/guide/example/example-json-15.kt
+++ b/guide/example/example-json-15.kt
@@ -6,13 +6,7 @@
 
 fun main() {
     val element = Json.parseToJsonElement("""
-        {
-            "name": "kotlinx.serialization",
-            "forks": [{"votes": 42}, {"votes": 9000}, {}]
-        }
+        {"name":"kotlinx.serialization","language":"Kotlin"}
     """)
-    val sum = element
-        .jsonObject["forks"]!!
-        .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
-    println(sum)
+    println(element)
 }
diff --git a/guide/example/example-json-16.kt b/guide/example/example-json-16.kt
index cff8ec7..fff287a 100644
--- a/guide/example/example-json-16.kt
+++ b/guide/example/example-json-16.kt
@@ -5,19 +5,14 @@
 import kotlinx.serialization.json.*
 
 fun main() {
-    val element = buildJsonObject {
-        put("name", "kotlinx.serialization")
-        putJsonObject("owner") {
-            put("name", "kotlin")
+    val element = Json.parseToJsonElement("""
+        {
+            "name": "kotlinx.serialization",
+            "forks": [{"votes": 42}, {"votes": 9000}, {}]
         }
-        putJsonArray("forks") {
-            addJsonObject {
-                put("votes", 42)
-            }
-            addJsonObject {
-                put("votes", 9000)
-            }
-        }
-    }
-    println(element)
+    """)
+    val sum = element
+        .jsonObject["forks"]!!
+        .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
+    println(sum)
 }
diff --git a/guide/example/example-json-17.kt b/guide/example/example-json-17.kt
index 25be758..72a696a 100644
--- a/guide/example/example-json-17.kt
+++ b/guide/example/example-json-17.kt
@@ -4,14 +4,20 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-@Serializable
-data class Project(val name: String, val language: String)
-
 fun main() {
     val element = buildJsonObject {
         put("name", "kotlinx.serialization")
-        put("language", "Kotlin")
+        putJsonObject("owner") {
+            put("name", "kotlin")
+        }
+        putJsonArray("forks") {
+            addJsonObject {
+                put("votes", 42)
+            }
+            addJsonObject {
+                put("votes", 9000)
+            }
+        }
     }
-    val data = Json.decodeFromJsonElement<Project>(element)
-    println(data)
+    println(element)
 }
diff --git a/guide/example/example-json-18.kt b/guide/example/example-json-18.kt
index 2a1add4..1b655bf 100644
--- a/guide/example/example-json-18.kt
+++ b/guide/example/example-json-18.kt
@@ -4,20 +4,14 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-import java.math.BigDecimal
-
-val format = Json { prettyPrint = true }
+@Serializable
+data class Project(val name: String, val language: String)
 
 fun main() {
-    val pi = BigDecimal("3.141592653589793238462643383279")
-    
-    val piJsonDouble = JsonPrimitive(pi.toDouble())
-    val piJsonString = JsonPrimitive(pi.toString())
-  
-    val piObject = buildJsonObject {
-        put("pi_double", piJsonDouble)
-        put("pi_string", piJsonString)
+    val element = buildJsonObject {
+        put("name", "kotlinx.serialization")
+        put("language", "Kotlin")
     }
-
-    println(format.encodeToString(piObject))
+    val data = Json.decodeFromJsonElement<Project>(element)
+    println(data)
 }
diff --git a/guide/example/example-json-19.kt b/guide/example/example-json-19.kt
index d59bf26..b001c55 100644
--- a/guide/example/example-json-19.kt
+++ b/guide/example/example-json-19.kt
@@ -10,15 +10,11 @@
 
 fun main() {
     val pi = BigDecimal("3.141592653589793238462643383279")
-
-    // use JsonUnquotedLiteral to encode raw JSON content
-    val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
-
+    
     val piJsonDouble = JsonPrimitive(pi.toDouble())
     val piJsonString = JsonPrimitive(pi.toString())
   
     val piObject = buildJsonObject {
-        put("pi_literal", piJsonLiteral)
         put("pi_double", piJsonDouble)
         put("pi_string", piJsonString)
     }
diff --git a/guide/example/example-json-20.kt b/guide/example/example-json-20.kt
index 2f481da..f522b3f 100644
--- a/guide/example/example-json-20.kt
+++ b/guide/example/example-json-20.kt
@@ -6,18 +6,22 @@
 
 import java.math.BigDecimal
 
+val format = Json { prettyPrint = true }
+
 fun main() {
-    val piObjectJson = """
-          {
-              "pi_literal": 3.141592653589793238462643383279
-          }
-      """.trimIndent()
-    
-    val piObject: JsonObject = Json.decodeFromString(piObjectJson)
-    
-    val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
-    
-    val pi = BigDecimal(piJsonLiteral)
-    
-    println(pi)
+    val pi = BigDecimal("3.141592653589793238462643383279")
+
+    // use JsonUnquotedLiteral to encode raw JSON content
+    val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
+
+    val piJsonDouble = JsonPrimitive(pi.toDouble())
+    val piJsonString = JsonPrimitive(pi.toString())
+  
+    val piObject = buildJsonObject {
+        put("pi_literal", piJsonLiteral)
+        put("pi_double", piJsonDouble)
+        put("pi_string", piJsonString)
+    }
+
+    println(format.encodeToString(piObject))
 }
diff --git a/guide/example/example-json-21.kt b/guide/example/example-json-21.kt
index 86a4b73..efd6071 100644
--- a/guide/example/example-json-21.kt
+++ b/guide/example/example-json-21.kt
@@ -4,7 +4,20 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
+import java.math.BigDecimal
+
 fun main() {
-    // caution: creating null with JsonUnquotedLiteral will cause an exception! 
-    JsonUnquotedLiteral("null")
+    val piObjectJson = """
+          {
+              "pi_literal": 3.141592653589793238462643383279
+          }
+      """.trimIndent()
+    
+    val piObject: JsonObject = Json.decodeFromString(piObjectJson)
+    
+    val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
+    
+    val pi = BigDecimal(piJsonLiteral)
+    
+    println(pi)
 }
diff --git a/guide/example/example-json-22.kt b/guide/example/example-json-22.kt
index 84bd0d8..e64ab06 100644
--- a/guide/example/example-json-22.kt
+++ b/guide/example/example-json-22.kt
@@ -4,29 +4,7 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-import kotlinx.serialization.builtins.*
-
-@Serializable
-data class Project(
-    val name: String,
-    @Serializable(with = UserListSerializer::class)
-    val users: List<User>
-)
-
-@Serializable
-data class User(val name: String)
-
-object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
-    // If response is not an array, then it is a single object that should be wrapped into the array
-    override fun transformDeserialize(element: JsonElement): JsonElement =
-        if (element !is JsonArray) JsonArray(listOf(element)) else element
-}
-
 fun main() {
-    println(Json.decodeFromString<Project>("""
-        {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
-    """))
-    println(Json.decodeFromString<Project>("""
-        {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
-    """))
+    // caution: creating null with JsonUnquotedLiteral will cause an exception! 
+    JsonUnquotedLiteral("null")
 }
diff --git a/guide/example/example-json-23.kt b/guide/example/example-json-23.kt
index bb23f52..ffa9f7d 100644
--- a/guide/example/example-json-23.kt
+++ b/guide/example/example-json-23.kt
@@ -17,14 +17,16 @@
 data class User(val name: String)
 
 object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
-
-    override fun transformSerialize(element: JsonElement): JsonElement {
-        require(element is JsonArray) // this serializer is used only with lists
-        return element.singleOrNull() ?: element
-    }
+    // If response is not an array, then it is a single object that should be wrapped into the array
+    override fun transformDeserialize(element: JsonElement): JsonElement =
+        if (element !is JsonArray) JsonArray(listOf(element)) else element
 }
 
 fun main() {
-    val data = Project("kotlinx.serialization", listOf(User("kotlin")))
-    println(Json.encodeToString(data))
+    println(Json.decodeFromString<Project>("""
+        {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
+    """))
+    println(Json.decodeFromString<Project>("""
+        {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
+    """))
 }
diff --git a/guide/example/example-json-24.kt b/guide/example/example-json-24.kt
index def90f2..010bd27 100644
--- a/guide/example/example-json-24.kt
+++ b/guide/example/example-json-24.kt
@@ -4,19 +4,27 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-@Serializable
-class Project(val name: String, val language: String)
+import kotlinx.serialization.builtins.*
 
-object ProjectSerializer : JsonTransformingSerializer<Project>(Project.serializer()) {
-    override fun transformSerialize(element: JsonElement): JsonElement =
-        // Filter out top-level key value pair with the key "language" and the value "Kotlin"
-        JsonObject(element.jsonObject.filterNot {
-            (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
-        })
+@Serializable
+data class Project(
+    val name: String,
+    @Serializable(with = UserListSerializer::class)
+    val users: List<User>
+)
+
+@Serializable
+data class User(val name: String)
+
+object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
+
+    override fun transformSerialize(element: JsonElement): JsonElement {
+        require(element is JsonArray) // this serializer is used only with lists
+        return element.singleOrNull() ?: element
+    }
 }
 
 fun main() {
-    val data = Project("kotlinx.serialization", "Kotlin")
-    println(Json.encodeToString(data)) // using plugin-generated serializer
-    println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer
+    val data = Project("kotlinx.serialization", listOf(User("kotlin")))
+    println(Json.encodeToString(data))
 }
diff --git a/guide/example/example-json-25.kt b/guide/example/example-json-25.kt
index 6f6d67a..a7d19a7 100644
--- a/guide/example/example-json-25.kt
+++ b/guide/example/example-json-25.kt
@@ -4,33 +4,19 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-import kotlinx.serialization.builtins.*
-
 @Serializable
-abstract class Project {
-    abstract val name: String
-}
+class Project(val name: String, val language: String)
 
-@Serializable
-data class BasicProject(override val name: String): Project()
-
-
-@Serializable
-data class OwnedProject(override val name: String, val owner: String) : Project()
-
-object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) {
-    override fun selectDeserializer(element: JsonElement) = when {
-        "owner" in element.jsonObject -> OwnedProject.serializer()
-        else -> BasicProject.serializer()
-    }
+object ProjectSerializer : JsonTransformingSerializer<Project>(Project.serializer()) {
+    override fun transformSerialize(element: JsonElement): JsonElement =
+        // Filter out top-level key value pair with the key "language" and the value "Kotlin"
+        JsonObject(element.jsonObject.filterNot {
+            (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
+        })
 }
 
 fun main() {
-    val data = listOf(
-        OwnedProject("kotlinx.serialization", "kotlin"),
-        BasicProject("example")
-    )
-    val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
-    println(string)
-    println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
+    val data = Project("kotlinx.serialization", "Kotlin")
+    println(Json.encodeToString(data)) // using plugin-generated serializer
+    println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer
 }
diff --git a/guide/example/example-json-26.kt b/guide/example/example-json-26.kt
index c308b63..b1b9299 100644
--- a/guide/example/example-json-26.kt
+++ b/guide/example/example-json-26.kt
@@ -4,56 +4,33 @@
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
 
-import kotlinx.serialization.descriptors.*
-import kotlinx.serialization.encoding.*
+import kotlinx.serialization.builtins.*
 
-@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>()
-}
-
-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) }
-        }
-        // JsonElement -> JsonEncoder
-        encoder.encodeJsonElement(element)
-    }
+@Serializable
+abstract class Project {
+    abstract val name: String
 }
 
 @Serializable
-data class Project(val name: String)
+data class BasicProject(override val name: String): Project()
+
+
+@Serializable
+data class OwnedProject(override val name: String, val owner: String) : Project()
+
+object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) {
+    override fun selectDeserializer(element: JsonElement) = when {
+        "owner" in element.jsonObject -> OwnedProject.serializer()
+        else -> BasicProject.serializer()
+    }
+}
 
 fun main() {
-    val responses = listOf(
-        Response.Ok(Project("kotlinx.serialization")),
-        Response.Error("Not found")
+    val data = listOf(
+        OwnedProject("kotlinx.serialization", "kotlin"),
+        BasicProject("example")
     )
-    val string = Json.encodeToString(responses)
+    val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
     println(string)
-    println(Json.decodeFromString<List<Response<Project>>>(string))
+    println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
 }
diff --git a/guide/example/example-json-27.kt b/guide/example/example-json-27.kt
index 219de6e..5905733 100644
--- a/guide/example/example-json-27.kt
+++ b/guide/example/example-json-27.kt
@@ -7,31 +7,53 @@
 import kotlinx.serialization.descriptors.*
 import kotlinx.serialization.encoding.*
 
-data class UnknownProject(val name: String, val details: JsonObject)
+@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>()
+}
 
-object UnknownProjectSerializer : KSerializer<UnknownProject> {
-    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
-        element<String>("name")
-        element<JsonElement>("details")
+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): 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 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: UnknownProject) {
-        error("Serialization is not supported")
+    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) }
+        }
+        // JsonElement -> JsonEncoder
+        encoder.encodeJsonElement(element)
     }
 }
 
+@Serializable
+data class Project(val name: String)
+
 fun main() {
-    println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
+    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))
 }
diff --git a/guide/example/example-json-28.kt b/guide/example/example-json-28.kt
new file mode 100644
index 0000000..a3fab61
--- /dev/null
+++ b/guide/example/example-json-28.kt
@@ -0,0 +1,37 @@
+// This file was automatically generated from json.md by Knit tool. Do not edit.
+package example.exampleJson28
+
+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 115ef77..0c5ed85 100644
--- a/guide/test/JsonTest.kt
+++ b/guide/test/JsonTest.kt
@@ -90,52 +90,49 @@
     @Test
     fun testExampleJson12() {
         captureOutput("ExampleJson12") { example.exampleJson12.main() }.verifyOutputLines(
-            "CasesList(cases=[VALUE_A, VALUE_B])"
+            "{\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}"
         )
     }
 
     @Test
     fun testExampleJson13() {
         captureOutput("ExampleJson13") { example.exampleJson13.main() }.verifyOutputLines(
-            "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}"
+            "CasesList(cases=[VALUE_A, VALUE_B])"
         )
     }
 
     @Test
     fun testExampleJson14() {
         captureOutput("ExampleJson14") { example.exampleJson14.main() }.verifyOutputLines(
-            "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
+            "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}"
         )
     }
 
     @Test
     fun testExampleJson15() {
         captureOutput("ExampleJson15") { example.exampleJson15.main() }.verifyOutputLines(
-            "9042"
+            "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
         )
     }
 
     @Test
     fun testExampleJson16() {
         captureOutput("ExampleJson16") { example.exampleJson16.main() }.verifyOutputLines(
-            "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}"
+            "9042"
         )
     }
 
     @Test
     fun testExampleJson17() {
         captureOutput("ExampleJson17") { example.exampleJson17.main() }.verifyOutputLines(
-            "Project(name=kotlinx.serialization, language=Kotlin)"
+            "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}"
         )
     }
 
     @Test
     fun testExampleJson18() {
         captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines(
-            "{",
-            "    \"pi_double\": 3.141592653589793,",
-            "    \"pi_string\": \"3.141592653589793238462643383279\"",
-            "}"
+            "Project(name=kotlinx.serialization, language=Kotlin)"
         )
     }
 
@@ -143,7 +140,6 @@
     fun testExampleJson19() {
         captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines(
             "{",
-            "    \"pi_literal\": 3.141592653589793238462643383279,",
             "    \"pi_double\": 3.141592653589793,",
             "    \"pi_string\": \"3.141592653589793238462643383279\"",
             "}"
@@ -153,59 +149,70 @@
     @Test
     fun testExampleJson20() {
         captureOutput("ExampleJson20") { example.exampleJson20.main() }.verifyOutputLines(
-            "3.141592653589793238462643383279"
+            "{",
+            "    \"pi_literal\": 3.141592653589793238462643383279,",
+            "    \"pi_double\": 3.141592653589793,",
+            "    \"pi_string\": \"3.141592653589793238462643383279\"",
+            "}"
         )
     }
 
     @Test
     fun testExampleJson21() {
-        captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLinesStart(
-            "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive"
+        captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines(
+            "3.141592653589793238462643383279"
         )
     }
 
     @Test
     fun testExampleJson22() {
-        captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLines(
-            "Project(name=kotlinx.serialization, users=[User(name=kotlin)])",
-            "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])"
+        captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLinesStart(
+            "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive"
         )
     }
 
     @Test
     fun testExampleJson23() {
         captureOutput("ExampleJson23") { example.exampleJson23.main() }.verifyOutputLines(
-            "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}"
+            "Project(name=kotlinx.serialization, users=[User(name=kotlin)])",
+            "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])"
         )
     }
 
     @Test
     fun testExampleJson24() {
         captureOutput("ExampleJson24") { example.exampleJson24.main() }.verifyOutputLines(
-            "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}",
-            "{\"name\":\"kotlinx.serialization\"}"
+            "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}"
         )
     }
 
     @Test
     fun testExampleJson25() {
         captureOutput("ExampleJson25") { example.exampleJson25.main() }.verifyOutputLines(
-            "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]",
-            "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]"
+            "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}",
+            "{\"name\":\"kotlinx.serialization\"}"
         )
     }
 
     @Test
     fun testExampleJson26() {
         captureOutput("ExampleJson26") { example.exampleJson26.main() }.verifyOutputLines(
-            "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]",
-            "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]"
+            "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]",
+            "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]"
         )
     }
 
     @Test
     fun testExampleJson27() {
         captureOutput("ExampleJson27") { example.exampleJson27.main() }.verifyOutputLines(
+            "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]",
+            "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]"
+        )
+    }
+
+    @Test
+    fun testExampleJson28() {
+        captureOutput("ExampleJson28") { example.exampleJson28.main() }.verifyOutputLines(
             "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})"
         )
     }