Merge remote-tracking branch 'origin/master' into dev
diff --git a/build.gradle b/build.gradle
index 73b566a..983e791 100644
--- a/build.gradle
+++ b/build.gradle
@@ -39,6 +39,10 @@
     ]
     ext.koverEnabled = property('kover.enabled') ?: true
 
+    def noTeamcityInteractionFlag = rootProject.hasProperty("no_teamcity_interaction")
+    def buildSnapshotUPFlag = rootProject.hasProperty("build_snapshot_up")
+    ext.teamcityInteractionDisabled = noTeamcityInteractionFlag || buildSnapshotUPFlag
+
     /*
     * This property group is used to build kotlinx.serialization against Kotlin compiler snapshot.
     * When build_snapshot_train is set to true, kotlin_version property is overridden with kotlin_snapshot_version.
diff --git a/buildSrc/src/main/kotlin/Java9Modularity.kt b/buildSrc/src/main/kotlin/Java9Modularity.kt
index 73621d8..e8c41fc 100644
--- a/buildSrc/src/main/kotlin/Java9Modularity.kt
+++ b/buildSrc/src/main/kotlin/Java9Modularity.kt
@@ -12,6 +12,7 @@
 import org.gradle.kotlin.dsl.*
 import org.gradle.language.base.plugins.LifecycleBasePlugin.*
 import org.gradle.process.*
+import org.jetbrains.kotlin.gradle.*
 import org.jetbrains.kotlin.gradle.dsl.*
 import org.jetbrains.kotlin.gradle.plugin.*
 import org.jetbrains.kotlin.gradle.plugin.mpp.*
@@ -128,7 +129,10 @@
             kotlinOptions {
                 moduleName = compileTask.kotlinOptions.moduleName
                 jvmTarget = "9"
-                freeCompilerArgs += "-Xjdk-release=9"
+                // To support LV override when set in aggregate builds
+                languageVersion = compileTask.kotlinOptions.languageVersion
+                freeCompilerArgs += listOf("-Xjdk-release=9",  "-Xsuppress-version-warnings", "-Xexpect-actual-classes")
+                options.optIn.addAll(compileTask.kotlinOptions.options.optIn)
             }
             // work-around for https://youtrack.jetbrains.com/issue/KT-60583
             inputs.files(
@@ -147,6 +151,11 @@
             // part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
             @Suppress("INVISIBLE_MEMBER")
             commonSourceSet.from(compileTask.commonSourceSet)
+            @OptIn(InternalKotlinGradlePluginApi::class)
+            apply {
+                multiplatformStructure.refinesEdges.set(compileTask.multiplatformStructure.refinesEdges)
+                multiplatformStructure.fragments.set(compileTask.multiplatformStructure.fragments)
+            }
             // part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
             // and work-around for https://youtrack.jetbrains.com/issue/KT-60582
             incremental = false
diff --git a/buildSrc/src/main/kotlin/KotlinVersion.kt b/buildSrc/src/main/kotlin/KotlinVersion.kt
new file mode 100644
index 0000000..5ac051e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/KotlinVersion.kt
@@ -0,0 +1,14 @@
+@file:JvmName("KotlinVersion")
+
+fun isKotlinVersionAtLeast(kotlinVersion: String, atLeastMajor: Int, atLeastMinor: Int, atLeastPatch: Int): Boolean {
+    val (major, minor) = kotlinVersion
+        .split('.')
+        .take(2)
+        .map { it.toInt() }
+    val patch = kotlinVersion.substringAfterLast('.').substringBefore('-').toInt()
+    return when {
+        major > atLeastMajor -> true
+        major < atLeastMajor -> false
+        else -> (minor == atLeastMinor && patch >= atLeastPatch) || minor > atLeastMinor
+    }
+}
diff --git a/core/build.gradle b/core/build.gradle
index a561b02..f52837a 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -66,3 +66,7 @@
 }
 
 Java9Modularity.configureJava9ModuleInfo(project)
+
+tasks.withType(org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrLink.class).configureEach {
+    kotlinOptions.freeCompilerArgs += "-Xwasm-enable-array-range-checks"
+}
diff --git a/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
index 7af9d9a..99f7d0a 100644
--- a/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
+++ b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
@@ -5,6 +5,7 @@
 package kotlinx.serialization
 
 import kotlinx.serialization.encoding.*
+import kotlinx.serialization.descriptors.*
 
 /**
  * A generic exception indicating the problem in serialization or deserialization process.
@@ -65,6 +66,12 @@
  * [MissingFieldException] is thrown on missing field from all [auto-generated][Serializable] serializers and it
  * is recommended to throw this exception from user-defined serializers.
  *
+ * [MissingFieldException] is constructed from the following properties:
+ * - [missingFields] -- fields that were required for the deserialization but have not been found.
+ *   They are always non-empty and their names match the corresponding names in [SerialDescriptor.elementNames]
+ * - Optional `serialName` -- serial name of the enclosing class that failed to get deserialized.
+ *   Matches the corresponding [SerialDescriptor.serialName].
+ *
  * @see SerializationException
  * @see KSerializer
  */
diff --git a/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt b/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt
index fad7ef8..ffe6dd3 100644
--- a/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt
+++ b/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt
@@ -74,8 +74,7 @@
         index: Int,
         deserializer: DeserializationStrategy<T?>,
         previousValue: T?
-    ): T? {
-        val isNullabilitySupported = deserializer.descriptor.isNullable
-        return if (isNullabilitySupported || decodeNotNullMark()) decodeSerializableValue(deserializer, previousValue) else decodeNull()
+    ): T? = decodeIfNullable(deserializer) {
+        decodeSerializableValue(deserializer, previousValue)
     }
 }
diff --git a/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt b/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt
index f29c805..dc4aa2a 100644
--- a/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt
+++ b/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt
@@ -260,12 +260,17 @@
      * Decodes the nullable value of type [T] by delegating the decoding process to the given [deserializer].
      */
     @ExperimentalSerializationApi
-    public fun <T : Any> decodeNullableSerializableValue(deserializer: DeserializationStrategy<T?>): T? {
-        val isNullabilitySupported = deserializer.descriptor.isNullable
-        return if (isNullabilitySupported || decodeNotNullMark()) decodeSerializableValue(deserializer) else decodeNull()
+    public fun <T : Any> decodeNullableSerializableValue(deserializer: DeserializationStrategy<T?>): T? = decodeIfNullable(deserializer) {
+        decodeSerializableValue(deserializer)
     }
 }
 
+@OptIn(ExperimentalSerializationApi::class)
+internal inline fun <T : Any> Decoder.decodeIfNullable(deserializer: DeserializationStrategy<T?>, block: () -> T?): T? {
+    val isNullabilitySupported = deserializer.descriptor.isNullable
+    return if (isNullabilitySupported || decodeNotNullMark()) block() else decodeNull()
+}
+
 /**
  * [CompositeDecoder] is a part of decoding process that is bound to a particular structured part of
  * the serialized form, described by the serial descriptor passed to [Decoder.beginStructure].
diff --git a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt
index 8604bbc..26d3b5e 100644
--- a/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt
+++ b/core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt
@@ -58,8 +58,8 @@
                 }
                 else -> throw SerializationException(
                     "Invalid index in polymorphic deserialization of " +
-                            (klassName ?: "unknown class") +
-                            "\n Expected 0, 1 or DECODE_DONE(-1), but found $index"
+                        (klassName ?: "unknown class") +
+                        "\n Expected 0, 1 or DECODE_DONE(-1), but found $index"
                 )
             }
         }
@@ -98,14 +98,14 @@
 
 @JvmName("throwSubtypeNotRegistered")
 internal fun throwSubtypeNotRegistered(subClassName: String?, baseClass: KClass<*>): Nothing {
-    val scope = "in the scope of '${baseClass.simpleName}'"
+    val scope = "in the polymorphic scope of '${baseClass.simpleName}'"
     throw SerializationException(
         if (subClassName == null)
-            "Class discriminator was missing and no default polymorphic serializers were registered $scope"
+            "Class discriminator was missing and no default serializers were registered $scope."
         else
-            "Class '$subClassName' is not registered for polymorphic serialization $scope.\n" +
-            "To be registered automatically, class '$subClassName' has to be '@Serializable', and the base class '${baseClass.simpleName}' has to be sealed and '@Serializable'.\n" +
-            "Alternatively, register the serializer for '$subClassName' explicitly in a corresponding SerializersModule."
+            "Serializer for subclass '$subClassName' is not found $scope.\n" +
+                "Check if class with serial name '$subClassName' exists and serializer is registered in a corresponding SerializersModule.\n" +
+                "To be registered automatically, class '$subClassName' has to be '@Serializable', and the base class '${baseClass.simpleName}' has to be sealed and '@Serializable'."
     )
 }
 
diff --git a/core/commonMain/src/kotlinx/serialization/internal/ElementMarker.kt b/core/commonMain/src/kotlinx/serialization/internal/ElementMarker.kt
index fca9026..5e6736d 100644
--- a/core/commonMain/src/kotlinx/serialization/internal/ElementMarker.kt
+++ b/core/commonMain/src/kotlinx/serialization/internal/ElementMarker.kt
@@ -4,13 +4,13 @@
 
 package kotlinx.serialization.internal
 
-import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.*
 import kotlinx.serialization.descriptors.SerialDescriptor
 import kotlinx.serialization.encoding.CompositeDecoder
 
 @OptIn(ExperimentalSerializationApi::class)
-@PublishedApi
-internal class ElementMarker(
+@CoreFriendModuleApi
+public class ElementMarker(
     private val descriptor: SerialDescriptor,
     // Instead of inheritance and virtual function in order to keep cross-module internal modifier via suppresses
     // Can be reworked via public + internal api if necessary
@@ -45,7 +45,7 @@
         }
     }
 
-    fun mark(index: Int) {
+    public fun mark(index: Int) {
         if (index < Long.SIZE_BITS) {
             lowerMarks = lowerMarks or (1L shl index)
         } else {
@@ -53,7 +53,7 @@
         }
     }
 
-    fun nextUnmarkedIndex(): Int {
+    public fun nextUnmarkedIndex(): Int {
         val elementsCount = descriptor.elementsCount
         while (lowerMarks != -1L) {
             val index = lowerMarks.inv().countTrailingZeroBits()
diff --git a/core/commonMain/src/kotlinx/serialization/internal/JsonInternalDependencies.kt b/core/commonMain/src/kotlinx/serialization/internal/JsonInternalDependencies.kt
index d79cb8b..e733827 100644
--- a/core/commonMain/src/kotlinx/serialization/internal/JsonInternalDependencies.kt
+++ b/core/commonMain/src/kotlinx/serialization/internal/JsonInternalDependencies.kt
@@ -1,14 +1,14 @@
 package kotlinx.serialization.internal
 
-import kotlinx.serialization.*
 import kotlinx.serialization.descriptors.*
 
 /*
- * Methods that are required for kotlinx-serialization-json, but are not effectively public
- * and actually represent our own technical debt.
- * This methods are not intended for public use
+ * Methods that are required for kotlinx-serialization-json, but are not effectively public.
+ *
+ * Anything marker with this annotation is not intended for public use.
  */
+@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
+internal annotation class CoreFriendModuleApi
 
-@InternalSerializationApi
-@Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
+@CoreFriendModuleApi
 public fun SerialDescriptor.jsonCachedSerialNames(): Set<String> = cachedSerialNames()
diff --git a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt
index 7b6efe7..a954bda 100644
--- a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt
+++ b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt
@@ -1,7 +1,7 @@
 /*
  * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
-@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "UNUSED")
+@file:Suppress("UNUSED")
 
 package kotlinx.serialization.internal
 
diff --git a/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt b/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt
index 3af5d35..cf71388 100644
--- a/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt
+++ b/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt
@@ -206,7 +206,6 @@
     protected open fun <T : Any?> decodeSerializableValue(deserializer: DeserializationStrategy<T>, previousValue: T?): T =
         decodeSerializableValue(deserializer)
 
-
     // ---- Implementation of low-level API ----
 
     override fun decodeInline(descriptor: SerialDescriptor): Decoder =
@@ -284,13 +283,11 @@
         index: Int,
         deserializer: DeserializationStrategy<T?>,
         previousValue: T?
-    ): T? =
-        tagBlock(descriptor.getTag(index)) {
-            if (decodeNotNullMark()) decodeSerializableValue(
-                deserializer,
-                previousValue
-            ) else decodeNull()
+    ): T? = tagBlock(descriptor.getTag(index)) {
+        decodeIfNullable(deserializer) {
+            decodeSerializableValue(deserializer, previousValue)
         }
+    }
 
     private fun <E> tagBlock(tag: Tag, block: () -> E): E {
         pushTag(tag)
diff --git a/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt b/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt
index 455a896..caa2768 100644
--- a/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt
+++ b/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt
@@ -10,6 +10,7 @@
 import kotlinx.serialization.encoding.*
 import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME
 import kotlinx.serialization.modules.*
+import kotlinx.serialization.test.*
 import kotlin.test.*
 import kotlin.time.Duration
 
diff --git a/core/commonTest/src/kotlinx/serialization/ElementMarkerTest.kt b/core/commonTest/src/kotlinx/serialization/ElementMarkerTest.kt
index a22be3f..3fee587 100644
--- a/core/commonTest/src/kotlinx/serialization/ElementMarkerTest.kt
+++ b/core/commonTest/src/kotlinx/serialization/ElementMarkerTest.kt
@@ -2,10 +2,11 @@
 
 import kotlinx.serialization.descriptors.*
 import kotlinx.serialization.encoding.CompositeDecoder
-import kotlinx.serialization.internal.ElementMarker
+import kotlinx.serialization.internal.*
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
+@OptIn(CoreFriendModuleApi::class)
 class ElementMarkerTest {
     @Test
     fun testNothingWasRead() {
diff --git a/core/commonTest/src/kotlinx/serialization/SerializersLookupInterfaceTest.kt b/core/commonTest/src/kotlinx/serialization/SerializersLookupInterfaceTest.kt
index 15c0351..fc77057 100644
--- a/core/commonTest/src/kotlinx/serialization/SerializersLookupInterfaceTest.kt
+++ b/core/commonTest/src/kotlinx/serialization/SerializersLookupInterfaceTest.kt
@@ -34,7 +34,7 @@
     @Test
     fun testInterfaceLookup() {
         // Native does not have KClass.isInterface
-        if (isNative()) return
+        if (isNative() || isWasm()) return
 
         val serializer1 = serializer<I>()
         assertTrue(serializer1 is PolymorphicSerializer)
diff --git a/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt b/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt
index 6c429db..65324c4 100644
--- a/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt
+++ b/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt
@@ -64,25 +64,25 @@
     fun test() {
         assertSame<KSerializer<*>>(Plain.serializer(), serializer(typeOf<Plain>()))
 
-        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
+        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false, onWasm = false) {
             assertSame<KSerializer<*>>(PlainSerializer, serializer(typeOf<PlainWithCustom>()))
         }
 
-        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
+        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false, onWasm = false) {
             assertEquals(
                 Parametrized.serializer(Int.serializer()).descriptor.toString(),
                 serializer(typeOf<Parametrized<Int>>()).descriptor.toString()
             )
         }
 
-        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
+        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false, onWasm = false) {
             assertEquals(
                 ParametrizedWithCustom.serializer(Int.serializer()).descriptor.toString(),
                 serializer(typeOf<ParametrizedWithCustom<Int>>()).descriptor.toString()
             )
         }
 
-        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
+        shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false, onWasm = false) {
             assertEquals(
                 SealedInterface.serializer().descriptor.toString(),
                 serializer(typeOf<SealedInterface>()).descriptor.toString()
@@ -91,7 +91,7 @@
 
         // should fail because annotation @NamedCompanion will be placed again by the compilation plugin
         // and they both will be placed into @Container annotation - thus they will be invisible to the runtime
-        shouldFail<SerializationException>(sinceKotlin = "1.9.20", onJs = false, onNative = false) {
+        shouldFail<SerializationException>(sinceKotlin = "1.9.20", onJs = false, onNative = false, onWasm = false) {
             serializer(typeOf<SealedInterfaceWithExplicitAnnotation>())
         }
     }
diff --git a/core/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt b/core/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt
index 24e4c52..ce9de63 100644
--- a/core/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt
+++ b/core/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt
@@ -86,4 +86,4 @@
         arrayOf(null, -1, -2),
         arrayOf(IntData(1), IntData(2))
     )
-)
+)
\ No newline at end of file
diff --git a/core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt b/core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt
index e9c7670..7bd35c1 100644
--- a/core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt
+++ b/core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt
@@ -29,6 +29,7 @@
     onJvm: Boolean = true,
     onJs: Boolean = true,
     onNative: Boolean = true,
+    onWasm: Boolean = true,
     test: () -> Unit
 ) {
     val args = mapOf(
@@ -36,7 +37,8 @@
         "before" to beforeKotlin,
         "onJvm" to onJvm,
         "onJs" to onJs,
-        "onNative" to onNative
+        "onNative" to onNative,
+        "onWasm" to onWasm
     )
 
     val sinceVersion = sinceKotlin?.toKotlinVersion()
@@ -45,7 +47,7 @@
     val version = (sinceVersion != null && currentKotlinVersion >= sinceVersion)
         || (beforeVersion != null && currentKotlinVersion < beforeVersion)
 
-    val platform = (isJvm() && onJvm) || (isJs() && onJs) || (isNative() && onNative)
+    val platform = (isJvm() && onJvm) || (isJs() && onJs) || (isNative() && onNative) || (isWasm() && onWasm)
 
     var error: Throwable? = null
     try {
@@ -164,6 +166,16 @@
             shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onJs = false) {
                 // no-op
             }
+        } else if (isWasm()) {
+            shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onWasm = false) {
+                // no-op
+            }
+            shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", onWasm = false) {
+                // no-op
+            }
+            shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onWasm = false) {
+                // no-op
+            }
         } else if (isNative()) {
             shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onNative = false) {
                 // no-op
diff --git a/core/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt b/core/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
index 92cf087..594ec0b 100644
--- a/core/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
+++ b/core/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
@@ -5,7 +5,7 @@
 package kotlinx.serialization.test
 
 enum class Platform {
-    JVM, JS, NATIVE
+    JVM, JS, NATIVE, WASM
 }
 
 public expect val currentPlatform: Platform
@@ -13,3 +13,4 @@
 public fun isJs(): Boolean = currentPlatform == Platform.JS
 public fun isJvm(): Boolean = currentPlatform == Platform.JVM
 public fun isNative(): Boolean = currentPlatform == Platform.NATIVE
+public fun isWasm(): Boolean = currentPlatform == Platform.WASM
diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt
index 399dbb2..72ec9ea 100644
--- a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt
+++ b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt
@@ -153,6 +153,9 @@
 }
 
 private fun <T : Any> Class<T>.findObjectSerializer(): KSerializer<T>? {
+    // Special case to avoid IllegalAccessException on Java11+ (#2449)
+    // There are no serializable objects in the stdlib anyway.
+    if (this.canonicalName?.let { it.startsWith("java.") || it.startsWith("kotlin.") } != false) return null
     // Check it is an object without using kotlin-reflect
     val field =
         declaredFields.singleOrNull { it.name == "INSTANCE" && it.type == this && Modifier.isStatic(it.modifiers) }
diff --git a/core/wasmMain/src/kotlinx/serialization/Serializers.kt b/core/wasmMain/src/kotlinx/serialization/Serializers.kt
new file mode 100644
index 0000000..a499392
--- /dev/null
+++ b/core/wasmMain/src/kotlinx/serialization/Serializers.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization
+
+import kotlin.reflect.*
+
+@OptIn(ExperimentalAssociatedObjects::class)
+@AssociatedObjectKey
+@Retention(AnnotationRetention.BINARY)
+@PublishedApi
+internal annotation class SerializableWith(public val serializer: KClass<out KSerializer<*>>)
\ No newline at end of file
diff --git a/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt b/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt
new file mode 100644
index 0000000..310df02
--- /dev/null
+++ b/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.internal
+
+import kotlinx.serialization.*
+import kotlin.reflect.*
+
+@Suppress("NOTHING_TO_INLINE")
+internal actual inline fun <T> Array<T>.getChecked(index: Int): T {
+    return get(index)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal actual inline fun BooleanArray.getChecked(index: Int): Boolean {
+    return get(index)
+}
+
+internal actual fun KClass<*>.platformSpecificSerializerNotRegistered(): Nothing {
+    throw SerializationException(
+        "${notRegisteredMessage()}\n" +
+                "On Kotlin/Wasm explicitly declared serializer should be used for interfaces and enums without @Serializable annotation"
+    )
+}
+
+@Suppress(
+    "UNCHECKED_CAST",
+    "DEPRECATION_ERROR"
+)
+@OptIn(ExperimentalAssociatedObjects::class)
+internal actual fun <T : Any> KClass<T>.constructSerializerForGivenTypeArgs(vararg args: KSerializer<Any?>): KSerializer<T>? =
+    when (val assocObject = findAssociatedObject<SerializableWith>()) {
+        is KSerializer<*> -> assocObject as KSerializer<T>
+        is SerializerFactory -> assocObject.serializer(*args) as KSerializer<T>
+        else -> null
+    }
+
+@Suppress("DEPRECATION_ERROR")
+internal actual fun <T : Any> KClass<T>.compiledSerializerImpl(): KSerializer<T>? =
+    this.constructSerializerForGivenTypeArgs()
+
+
+internal actual fun <T> createCache(factory: (KClass<*>) -> KSerializer<T>?): SerializerCache<T> {
+    return object: SerializerCache<T> {
+        override fun get(key: KClass<Any>): KSerializer<T>? {
+            return factory(key)
+        }
+    }
+}
+
+internal actual fun <T> createParametrizedCache(factory: (KClass<Any>, List<KType>) -> KSerializer<T>?): ParametrizedSerializerCache<T> {
+    return object: ParametrizedSerializerCache<T> {
+        override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> {
+            return kotlin.runCatching { factory(key, types) }
+        }
+    }
+}
+
+internal actual fun <T : Any, E : T?> ArrayList<E>.toNativeArrayImpl(eClass: KClass<T>): Array<E> = toTypedArray()
+
+internal actual fun isReferenceArray(rootClass: KClass<Any>): Boolean = rootClass == Array::class
\ No newline at end of file
diff --git a/core/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt b/core/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt
new file mode 100644
index 0000000..fd359b7
--- /dev/null
+++ b/core/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt
@@ -0,0 +1,7 @@
+/*
+ * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.test
+
+public actual val currentPlatform: Platform = Platform.WASM
\ No newline at end of file
diff --git a/docs/polymorphism.md b/docs/polymorphism.md
index 29d023b..b7ea31f 100644
--- a/docs/polymorphism.md
+++ b/docs/polymorphism.md
@@ -123,9 +123,9 @@
 This is close to the best design for a serializable hierarchy of classes, but running it produces the following error:
 
 ```text 
-Exception in thread "main" kotlinx.serialization.SerializationException: Class 'OwnedProject' is not registered for polymorphic serialization in the scope of 'Project'.
+Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for subclass 'OwnedProject' is not found in the polymorphic scope of 'Project'.
+Check if class with serial name 'OwnedProject' exists and serializer is registered in a corresponding SerializersModule.
 To be registered automatically, class 'OwnedProject' has to be '@Serializable', and the base class 'Project' has to be sealed and '@Serializable'.
-Alternatively, register the serializer for 'OwnedProject' explicitly in a corresponding SerializersModule.
 ```         
 
 <!--- TEST LINES_START -->
@@ -832,7 +832,8 @@
 We get the following exception.
 
 ```text 
-Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for class discriminator 'unknown'
+Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 0: Serializer for subclass 'unknown' is not found in the polymorphic scope of 'Project' at path: $
+Check if class with serial name 'unknown' exists and serializer is registered in a corresponding SerializersModule.
 ```
 
 <!--- TEST LINES_START -->
diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt
index 0b7a0e0..b77a18c 100644
--- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt
+++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt
@@ -70,6 +70,8 @@
         if (encodeByteArrayAsByteString && serializer.descriptor == ByteArraySerializer().descriptor) {
             encoder.encodeByteString(value as ByteArray)
         } else {
+            encodeByteArrayAsByteString = encodeByteArrayAsByteString || serializer.descriptor.isInlineByteString()
+
             super.encodeSerializableValue(serializer, value)
         }
     }
@@ -278,6 +280,7 @@
             @Suppress("UNCHECKED_CAST")
             decoder.nextByteString() as T
         } else {
+            decodeByteArrayAsByteString = decodeByteArrayAsByteString || deserializer.descriptor.isInlineByteString()
             super.decodeSerializableValue(deserializer)
         }
     }
@@ -636,6 +639,11 @@
     return getElementAnnotations(index).find { it is ByteString } != null
 }
 
+private fun SerialDescriptor.isInlineByteString(): Boolean {
+    // inline item classes should only have 1 item
+    return isInline && isByteString(0)
+}
+
 
 private val normalizeBaseBits = SINGLE_PRECISION_NORMALIZE_BASE.toBits()
 
diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborReaderTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborReaderTest.kt
index edbe5e6..f615d5e 100644
--- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborReaderTest.kt
+++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborReaderTest.kt
@@ -634,6 +634,34 @@
     }
 
     @Test
+    fun testReadValueClassWithByteString() {
+        assertContentEquals(
+            expected = byteArrayOf(0x11, 0x22, 0x33),
+            actual = Cbor.decodeFromHexString<ValueClassWithByteString>("43112233").x
+        )
+    }
+
+    @Test
+    fun testReadValueClassCustomByteString() {
+        assertEquals(
+            expected = ValueClassWithCustomByteString(CustomByteString(0x11, 0x22, 0x33)),
+            actual = Cbor.decodeFromHexString("43112233")
+        )
+    }
+
+    @Test
+    fun testReadValueClassWithUnlabeledByteString() {
+        assertContentEquals(
+            expected = byteArrayOf(
+                0x11,
+                0x22,
+                0x33
+            ),
+            actual = Cbor.decodeFromHexString<ValueClassWithUnlabeledByteString>("43112233").x.x
+        )
+    }
+
+    @Test
     fun testIgnoresTagsOnStrings() {
         /*
          * 84                                # array(4)
diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt
index c546bdf..da7b128 100644
--- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt
+++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborWriterTest.kt
@@ -122,4 +122,28 @@
                 actual = Cbor.encodeToHexString(TypeWithNullableCustomByteString(null))
         )
     }
+
+    @Test
+    fun testWriteValueClassWithByteString() {
+        assertEquals(
+            expected = "43112233",
+            actual = Cbor.encodeToHexString(ValueClassWithByteString(byteArrayOf(0x11, 0x22, 0x33)))
+        )
+    }
+
+    @Test
+    fun testWriteValueClassCustomByteString() {
+        assertEquals(
+            expected = "43112233",
+            actual = Cbor.encodeToHexString(ValueClassWithCustomByteString(CustomByteString(0x11, 0x22, 0x33)))
+        )
+    }
+
+    @Test
+    fun testWriteValueClassWithUnlabeledByteString() {
+        assertEquals(
+            expected = "43112233",
+            actual = Cbor.encodeToHexString(ValueClassWithUnlabeledByteString(ValueClassWithUnlabeledByteString.Inner(byteArrayOf(0x11, 0x22, 0x33))))
+        )
+    }
 }
diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt
index ad55d04..e4418f4 100644
--- a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt
+++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/SampleClasses.kt
@@ -8,6 +8,7 @@
 import kotlinx.serialization.builtins.*
 import kotlinx.serialization.descriptors.*
 import kotlinx.serialization.encoding.*
+import kotlin.jvm.*
 
 @Serializable
 data class Simple(val a: String)
@@ -110,4 +111,20 @@
 data class TypeWithCustomByteString(@ByteString val x: CustomByteString)
 
 @Serializable
-data class TypeWithNullableCustomByteString(@ByteString val x: CustomByteString?)
\ No newline at end of file
+data class TypeWithNullableCustomByteString(@ByteString val x: CustomByteString?)
+
+@JvmInline
+@Serializable
+value class ValueClassWithByteString(@ByteString val x: ByteArray)
+
+@JvmInline
+@Serializable
+value class ValueClassWithCustomByteString(@ByteString val x: CustomByteString)
+
+@JvmInline
+@Serializable
+value class ValueClassWithUnlabeledByteString(@ByteString val x: Inner) {
+    @JvmInline
+    @Serializable
+    value class Inner(val x: ByteArray)
+}
\ No newline at end of file
diff --git a/formats/json-okio/build.gradle.kts b/formats/json-okio/build.gradle.kts
index 8d29e2e..a51fff0 100644
--- a/formats/json-okio/build.gradle.kts
+++ b/formats/json-okio/build.gradle.kts
@@ -15,6 +15,12 @@
 
 kotlin {
     sourceSets {
+        configureEach {
+            languageSettings {
+                optIn("kotlinx.serialization.internal.CoreFriendModuleApi")
+                optIn("kotlinx.serialization.json.internal.JsonFriendModuleApi")
+            }
+        }
         val commonMain by getting {
             dependencies {
                 api(project(":kotlinx-serialization-core"))
@@ -44,3 +50,13 @@
         }
     }
 }
+
+
+// TODO: Remove this after okio will be updated to the version with 1.9.20 stdlib dependency
+configurations.all {
+    resolutionStrategy.eachDependency {
+        if (requested.name == "kotlin-stdlib-wasm") {
+            useTarget("org.jetbrains.kotlin:kotlin-stdlib-wasm-js:${requested.version}")
+        }
+    }
+}
diff --git a/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/OkioStreams.kt b/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/OkioStreams.kt
index b827806..968f533 100644
--- a/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/OkioStreams.kt
+++ b/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/OkioStreams.kt
@@ -2,8 +2,6 @@
  * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
-@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-
 package kotlinx.serialization.json.okio
 
 import kotlinx.serialization.*
@@ -29,7 +27,7 @@
 ) {
     val writer = JsonToOkioStreamWriter(sink)
     try {
-        encodeByWriter(writer, serializer, value)
+        encodeByWriter(this, writer, serializer, value)
     } finally {
         writer.release()
     }
@@ -62,7 +60,7 @@
     deserializer: DeserializationStrategy<T>,
     source: BufferedSource
 ): T {
-    return decodeByReader(deserializer, OkioSerialReader(source))
+    return decodeByReader(this, deserializer, OkioSerialReader(source))
 }
 
 /**
@@ -101,7 +99,7 @@
     deserializer: DeserializationStrategy<T>,
     format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
 ): Sequence<T> {
-    return decodeToSequenceByReader(OkioSerialReader(source), deserializer, format)
+    return decodeToSequenceByReader(this, OkioSerialReader(source), deserializer, format)
 }
 
 /**
diff --git a/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/internal/OkioJsonStreams.kt b/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/internal/OkioJsonStreams.kt
index c206678..1de8971 100644
--- a/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/internal/OkioJsonStreams.kt
+++ b/formats/json-okio/commonMain/src/kotlinx/serialization/json/okio/internal/OkioJsonStreams.kt
@@ -2,16 +2,39 @@
  * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
-@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
-
 package kotlinx.serialization.json.okio.internal
 
-import kotlinx.serialization.json.internal.ESCAPE_STRINGS
-import kotlinx.serialization.json.internal.JsonWriter
-import kotlinx.serialization.json.internal.SerialReader
+import kotlinx.serialization.json.internal.*
 import okio.*
 
-internal class JsonToOkioStreamWriter(private val sink: BufferedSink) : JsonWriter {
+// Copied from kotlinx/serialization/json/internal/StringOps.kt
+private fun toHexChar(i: Int) : Char {
+    val d = i and 0xf
+    return if (d < 10) (d + '0'.code).toChar()
+    else (d - 10 + 'a'.code).toChar()
+}
+
+// Copied from kotlinx/serialization/json/internal/StringOps.kt
+private val ESCAPE_STRINGS: Array<String?> = arrayOfNulls<String>(93).apply {
+    for (c in 0..0x1f) {
+        val c1 = toHexChar(c shr 12)
+        val c2 = toHexChar(c shr 8)
+        val c3 = toHexChar(c shr 4)
+        val c4 = toHexChar(c)
+        this[c] = "\\u$c1$c2$c3$c4"
+    }
+    this['"'.code] = "\\\""
+    this['\\'.code] = "\\\\"
+    this['\t'.code] = "\\t"
+    this['\b'.code] = "\\b"
+    this['\n'.code] = "\\n"
+    this['\r'.code] = "\\r"
+    this[0x0c] = "\\f"
+}
+
+
+
+internal class JsonToOkioStreamWriter(private val sink: BufferedSink) : InternalJsonWriter {
     override fun writeLong(value: Long) {
         write(value.toString())
     }
@@ -54,7 +77,7 @@
 private const val LOW_SURROGATE_HEADER = 0xdc00
 
 
-internal class OkioSerialReader(private val source: BufferedSource): SerialReader {
+internal class OkioSerialReader(private val source: BufferedSource): InternalJsonReader {
     /*
     A sequence of code points is read from UTF-8, some of it can take 2 characters.
     In case the last code point requires 2 characters, and the array is already full, we buffer the second character
diff --git a/formats/json-tests/build.gradle.kts b/formats/json-tests/build.gradle.kts
index 9ae4247..6be0a3a 100644
--- a/formats/json-tests/build.gradle.kts
+++ b/formats/json-tests/build.gradle.kts
@@ -2,7 +2,7 @@
  * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 import Java9Modularity.configureJava9ModuleInfo
-import org.jetbrains.kotlin.gradle.tasks.*
+import org.jetbrains.kotlin.gradle.targets.js.testing.*
 
 plugins {
     kotlin("multiplatform")
@@ -25,6 +25,12 @@
 
 kotlin {
     sourceSets {
+        configureEach {
+            languageSettings {
+                optIn("kotlinx.serialization.internal.CoreFriendModuleApi")
+                optIn("kotlinx.serialization.json.internal.JsonFriendModuleApi")
+            }
+        }
         val commonTest by getting {
             dependencies {
                 api(project(":kotlinx-serialization-json"))
@@ -43,3 +49,12 @@
 }
 
 project.configureJava9ModuleInfo()
+
+// TODO: Remove this after okio will be updated to the version with 1.9.20 stdlib dependency
+configurations.all {
+    resolutionStrategy.eachDependency {
+        if (requested.name == "kotlin-stdlib-wasm") {
+            useTarget("org.jetbrains.kotlin:kotlin-stdlib-wasm-js:${requested.version}")
+        }
+    }
+}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt
index 52ab0f2..f878c63 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/UmbrellaTypes.kt
@@ -84,3 +84,24 @@
         arrayOf(IntData(1), IntData(2))
     )
 )
+
+val umbrellaInstance2 = TypesUmbrella(
+    Unit, true, 10, 20, 30, 40, 50.5f, 60.5, 'A', "Str0", Attitude.POSITIVE, IntData(70),
+    null, null, 11, 21, 31, 41, 51.5f, 61.5, 'B', "Str1", Attitude.NEUTRAL, null,
+    listOf(1, 2, 3),
+    listOf(4, 5, null),
+    setOf(6, 7, 8),
+    mutableSetOf(null, 9, 10),
+    listOf(listOf(Attitude.NEGATIVE, null)),
+    listOf(IntData(1), IntData(2), IntData(3)),
+    mutableListOf(IntData(1), null, IntData(3)),
+    Tree("root", Tree("left"), Tree("right", Tree("right.left"), Tree("right.right"))),
+    mapOf("one" to 1, "two" to 2, "three" to 3),
+    mapOf(0 to null, 1 to "first", 2 to "second"),
+    ArraysUmbrella(
+        arrayOf(1, 2, 3),
+        arrayOf(100, 200, 300),
+        arrayOf(null, -1, -2),
+        arrayOf(IntData(1), IntData(2))
+    )
+)
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicDeserializationErrorMessagesTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicDeserializationErrorMessagesTest.kt
new file mode 100644
index 0000000..2b2f1f7
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicDeserializationErrorMessagesTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.features
+
+import kotlinx.serialization.*
+import kotlinx.serialization.json.*
+import kotlin.test.*
+
+class PolymorphicDeserializationErrorMessagesTest : JsonTestBase() {
+    @Serializable
+    class DummyData(@Polymorphic val a: Any)
+
+    @Serializable
+    class Holder(val d: DummyData)
+
+    // TODO: remove this after #2480 is merged
+    private fun checkSerializationException(action: () -> Unit, assertions: SerializationException.(String) -> Unit) {
+        val e = assertFailsWith(SerializationException::class, action)
+        assertNotNull(e.message)
+        e.assertions(e.message!!)
+    }
+
+    @Test
+    fun testNotRegisteredMessage() = parametrizedTest { mode ->
+        val input = """{"d":{"a":{"type":"my.Class", "value":42}}}"""
+        checkSerializationException({
+            default.decodeFromString<Holder>(input, mode)
+        }, { message ->
+            // ReaderJsonLexer.peekLeadingMatchingValue is not implemented, so first-key optimization is not working for streaming yet.
+            if (mode == JsonTestingMode.STREAMING)
+                assertContains(message, "Unexpected JSON token at offset 10: Serializer for subclass 'my.Class' is not found in the polymorphic scope of 'Any' at path: \$.d.a")
+            else
+                assertContains(message, "Serializer for subclass 'my.Class' is not found in the polymorphic scope of 'Any'")
+        })
+    }
+
+    @Test
+    fun testDiscriminatorMissingNoDefaultMessage() = parametrizedTest { mode ->
+        val input = """{"d":{"a":{"value":42}}}"""
+        checkSerializationException({
+            default.decodeFromString<Holder>(input, mode)
+        }, { message ->
+            // Always slow path when discriminator is missing, so no position and path
+            assertContains(message, "Class discriminator was missing and no default serializers were registered in the polymorphic scope of 'Any'")
+        })
+    }
+
+    @Test
+    fun testClassDiscriminatorIsNull() = parametrizedTest { mode ->
+        val input = """{"d":{"a":{"type":null, "value":42}}}"""
+        checkSerializationException({
+            default.decodeFromString<Holder>(input, mode)
+        }, { message ->
+            // Always slow path when discriminator is missing, so no position and path
+            assertContains(message, "Class discriminator was missing and no default serializers were registered in the polymorphic scope of 'Any'")
+        })
+    }
+}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicOnClassesTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicOnClassesTest.kt
index ea59f32..77004db 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicOnClassesTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicOnClassesTest.kt
@@ -136,7 +136,7 @@
     fun testSerializerLookupForInterface() {
         // On JVM and JS IR it can be supported via reflection/runtime hacks
         // on Native, unfortunately, only with intrinsics.
-        if (isNative()) return
+        if (isNative() || isWasm()) return
         val msgSer = serializer<IMessage>()
         assertEquals(IMessage::class, (msgSer as AbstractPolymorphicSerializer).baseClass)
     }
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphismWithAnyTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphismWithAnyTest.kt
index e1d38fd..07b6e31 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphismWithAnyTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphismWithAnyTest.kt
@@ -5,13 +5,13 @@
 package kotlinx.serialization.features
 
 import kotlinx.serialization.*
-import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.*
 import kotlinx.serialization.modules.*
 import kotlinx.serialization.modules.plus
 import kotlinx.serialization.test.assertStringFormAndRestored
 import kotlin.test.*
 
-class PolymorphismWithAnyTest {
+class PolymorphismWithAnyTest: JsonTestBase() {
 
     @Serializable
     data class MyPolyData(val data: Map<String, @Polymorphic Any>)
@@ -28,19 +28,20 @@
         val className = className.substringAfterLast('.')
         val scopeName = scopeName.substringAfterLast('.')
         val expectedText =
-            "Class '$className' is not registered for polymorphic serialization in the scope of '$scopeName'"
+            "Serializer for subclass '$className' is not found in the polymorphic scope of '$scopeName'"
         assertTrue(exception.message!!.startsWith(expectedText),
             "Found $exception, but expected to start with: $expectedText")
     }
 
     @Test
-    fun testFailWithoutModulesWithCustomClass() {
+    fun testFailWithoutModulesWithCustomClass() = parametrizedTest { mode ->
         checkNotRegisteredMessage(
             "kotlinx.serialization.IntData", "kotlin.Any",
             assertFailsWith<SerializationException>("not registered") {
                 Json.encodeToString(
                     MyPolyData.serializer(),
-                    MyPolyData(mapOf("a" to IntData(42)))
+                    MyPolyData(mapOf("a" to IntData(42))),
+                    mode
                 )
             }
         )
@@ -51,11 +52,11 @@
         val json = Json {
             serializersModule = SerializersModule { polymorphic(Any::class) { subclass(IntData.serializer()) } }
         }
-        assertStringFormAndRestored(
+        assertJsonFormAndRestored(
             expected = """{"data":{"a":{"type":"kotlinx.serialization.IntData","intV":42}}}""",
-            original = MyPolyData(mapOf("a" to IntData(42))),
+            data = MyPolyData(mapOf("a" to IntData(42))),
             serializer = MyPolyData.serializer(),
-            format = json
+            json = json
         )
     }
 
@@ -63,14 +64,15 @@
      * This test should fail because PolyDerived registered in the scope of PolyBase, not kotlin.Any
      */
     @Test
-    fun testFailWithModulesNotInAnyScope() {
+    fun testFailWithModulesNotInAnyScope() = parametrizedTest { mode ->
         val json = Json { serializersModule = BaseAndDerivedModule }
         checkNotRegisteredMessage(
             "kotlinx.serialization.PolyDerived", "kotlin.Any",
             assertFailsWith<SerializationException> {
                 json.encodeToString(
                     MyPolyData.serializer(),
-                    MyPolyData(mapOf("a" to PolyDerived("foo")))
+                    MyPolyData(mapOf("a" to PolyDerived("foo"))),
+                    mode
                 )
             }
         )
@@ -86,11 +88,11 @@
     @Test
     fun testRebindModules() {
         val json = Json { serializersModule = baseAndDerivedModuleAtAny }
-        assertStringFormAndRestored(
+        assertJsonFormAndRestored(
             expected = """{"data":{"a":{"type":"kotlinx.serialization.PolyDerived","id":1,"s":"foo"}}}""",
-            original = MyPolyData(mapOf("a" to PolyDerived("foo"))),
+            data = MyPolyData(mapOf("a" to PolyDerived("foo"))),
             serializer = MyPolyData.serializer(),
-            format = json
+            json = json
         )
     }
 
@@ -98,7 +100,7 @@
      * This test should fail because PolyDerived registered in the scope of kotlin.Any, not PolyBase
      */
     @Test
-    fun testFailWithModulesNotInParticularScope() {
+    fun testFailWithModulesNotInParticularScope() = parametrizedTest { mode ->
         val json = Json { serializersModule = baseAndDerivedModuleAtAny }
         checkNotRegisteredMessage(
             "kotlinx.serialization.PolyDerived", "kotlinx.serialization.PolyBase",
@@ -108,7 +110,8 @@
                     MyPolyDataWithPolyBase(
                         mapOf("a" to PolyDerived("foo")),
                         PolyDerived("foo")
-                    )
+                    ),
+                    mode
                 )
             }
         )
@@ -117,17 +120,30 @@
     @Test
     fun testBindModules() {
         val json = Json { serializersModule = (baseAndDerivedModuleAtAny + BaseAndDerivedModule) }
-        assertStringFormAndRestored(
+        assertJsonFormAndRestored(
             expected = """{"data":{"a":{"type":"kotlinx.serialization.PolyDerived","id":1,"s":"foo"}},
                 |"polyBase":{"type":"kotlinx.serialization.PolyDerived","id":1,"s":"foo"}}""".trimMargin().lines().joinToString(
                 ""
             ),
-            original = MyPolyDataWithPolyBase(
+            data = MyPolyDataWithPolyBase(
                 mapOf("a" to PolyDerived("foo")),
                 PolyDerived("foo")
             ),
             serializer = MyPolyDataWithPolyBase.serializer(),
-            format = json
+            json = json
         )
     }
+
+    @Test
+    fun testTypeKeyLastInInput() = parametrizedTest { mode ->
+        val json = Json { serializersModule = (baseAndDerivedModuleAtAny + BaseAndDerivedModule) }
+        val input = """{"data":{"a":{"id":1,"s":"foo","type":"kotlinx.serialization.PolyDerived"}},
+                |"polyBase":{"id":1,"s":"foo","type":"kotlinx.serialization.PolyDerived"}}""".trimMargin().lines().joinToString(
+            "")
+        val data = MyPolyDataWithPolyBase(
+            mapOf("a" to PolyDerived("foo")),
+            PolyDerived("foo")
+        )
+        assertEquals(data, json.decodeFromString(MyPolyDataWithPolyBase.serializer(), input, mode))
+    }
 }
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/BasicTypesSerializationTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/BasicTypesSerializationTest.kt
index dee87fd..4959b7e 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/BasicTypesSerializationTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/BasicTypesSerializationTest.kt
@@ -5,24 +5,37 @@
 package kotlinx.serialization.json
 
 import kotlinx.serialization.*
+import kotlinx.serialization.test.*
 import kotlin.test.*
 
 class BasicTypesSerializationTest : JsonTestBase() {
 
     val goldenValue = """
-        {"unit":{},"boolean":true,"byte":10,"short":20,"int":30,"long":40,"float":50.1,"double":60.1,"char":"A","string":"Str0","enum":"POSITIVE","intData":{"intV":70},"unitN":null,"booleanN":null,"byteN":11,"shortN":21,"intN":31,"longN":41,"floatN":51.1,"doubleN":61.1,"charN":"B","stringN":"Str1","enumN":"NEUTRAL","intDataN":null,"listInt":[1,2,3],"listIntN":[4,5,null],"listNInt":[6,7,8],"listNIntN":[null,9,10],"listListEnumN":[["NEGATIVE",null]],"listIntData":[{"intV":1},{"intV":2},{"intV":3}],"listIntDataN":[{"intV":1},null,{"intV":3}],"tree":{"name":"root","left":{"name":"left","left":null,"right":null},"right":{"name":"right","left":{"name":"right.left","left":null,"right":null},"right":{"name":"right.right","left":null,"right":null}}},"mapStringInt":{"one":1,"two":2,"three":3},"mapIntStringN":{"0":null,"1":"first","2":"second"},"arrays":{"arrByte":[1,2,3],"arrInt":[100,200,300],"arrIntN":[null,-1,-2],"arrIntData":[{"intV":1},{"intV":2}]}}
+                {"unit":{},"boolean":true,"byte":10,"short":20,"int":30,"long":40,"float":50.1,"double":60.1,"char":"A","string":"Str0","enum":"POSITIVE","intData":{"intV":70},"unitN":null,"booleanN":null,"byteN":11,"shortN":21,"intN":31,"longN":41,"floatN":51.1,"doubleN":61.1,"charN":"B","stringN":"Str1","enumN":"NEUTRAL","intDataN":null,"listInt":[1,2,3],"listIntN":[4,5,null],"listNInt":[6,7,8],"listNIntN":[null,9,10],"listListEnumN":[["NEGATIVE",null]],"listIntData":[{"intV":1},{"intV":2},{"intV":3}],"listIntDataN":[{"intV":1},null,{"intV":3}],"tree":{"name":"root","left":{"name":"left","left":null,"right":null},"right":{"name":"right","left":{"name":"right.left","left":null,"right":null},"right":{"name":"right.right","left":null,"right":null}}},"mapStringInt":{"one":1,"two":2,"three":3},"mapIntStringN":{"0":null,"1":"first","2":"second"},"arrays":{"arrByte":[1,2,3],"arrInt":[100,200,300],"arrIntN":[null,-1,-2],"arrIntData":[{"intV":1},{"intV":2}]}}
     """.trimIndent()
 
-    @Test
-    fun testSerialization() = parametrizedTest { jsonTestingMode ->
-        val json = default.encodeToString(TypesUmbrella.serializer(), umbrellaInstance)
+    val goldenValue2 = """
+        {"unit":{},"boolean":true,"byte":10,"short":20,"int":30,"long":40,"float":50.5,"double":60.5,"char":"A","string":"Str0","enum":"POSITIVE","intData":{"intV":70},"unitN":null,"booleanN":null,"byteN":11,"shortN":21,"intN":31,"longN":41,"floatN":51.5,"doubleN":61.5,"charN":"B","stringN":"Str1","enumN":"NEUTRAL","intDataN":null,"listInt":[1,2,3],"listIntN":[4,5,null],"listNInt":[6,7,8],"listNIntN":[null,9,10],"listListEnumN":[["NEGATIVE",null]],"listIntData":[{"intV":1},{"intV":2},{"intV":3}],"listIntDataN":[{"intV":1},null,{"intV":3}],"tree":{"name":"root","left":{"name":"left","left":null,"right":null},"right":{"name":"right","left":{"name":"right.left","left":null,"right":null},"right":{"name":"right.right","left":null,"right":null}}},"mapStringInt":{"one":1,"two":2,"three":3},"mapIntStringN":{"0":null,"1":"first","2":"second"},"arrays":{"arrByte":[1,2,3],"arrInt":[100,200,300],"arrIntN":[null,-1,-2],"arrIntData":[{"intV":1},{"intV":2}]}}
+    """.trimIndent()
+
+    private fun testSerializationImpl(typesUmbrella: TypesUmbrella, goldenValue: String) = parametrizedTest { jsonTestingMode ->
+        val json = default.encodeToString(TypesUmbrella.serializer(), typesUmbrella)
         assertEquals(goldenValue, json)
         val instance = default.decodeFromString(TypesUmbrella.serializer(), json, jsonTestingMode)
-        assertEquals(umbrellaInstance, instance)
-        assertNotSame(umbrellaInstance, instance)
+        assertEquals(typesUmbrella, instance)
+        assertNotSame(typesUmbrella, instance)
     }
 
     @Test
+    fun testSerialization() {
+        if (isWasm()) return //https://youtrack.jetbrains.com/issue/KT-59118/WASM-floating-point-toString-inconsistencies
+        testSerializationImpl(umbrellaInstance, goldenValue)
+    }
+
+    @Test
+    fun testSerialization2() = testSerializationImpl(umbrellaInstance2, goldenValue2)
+
+    @Test
     fun testTopLevelPrimitive() = parametrizedTest { jsonTestingMode ->
         testPrimitive(Unit, "{}", jsonTestingMode)
         testPrimitive(false, "false", jsonTestingMode)
@@ -30,8 +43,8 @@
         testPrimitive(2.toShort(), "2", jsonTestingMode)
         testPrimitive(3, "3", jsonTestingMode)
         testPrimitive(4L, "4", jsonTestingMode)
-        testPrimitive(5.1f, "5.1", jsonTestingMode)
-        testPrimitive(6.1, "6.1", jsonTestingMode)
+        testPrimitive(2.5f, "2.5", jsonTestingMode)
+        testPrimitive(3.5, "3.5", jsonTestingMode)
         testPrimitive('c', "\"c\"", jsonTestingMode)
         testPrimitive("string", "\"string\"", jsonTestingMode)
     }
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonElementDecodingTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonElementDecodingTest.kt
index d0b105a..3cdfa08 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonElementDecodingTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonElementDecodingTest.kt
@@ -1,6 +1,8 @@
 package kotlinx.serialization.json
 
-import kotlinx.serialization.Serializable
+import kotlinx.serialization.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
 import kotlin.test.*
 
 class JsonElementDecodingTest : JsonTestBase() {
@@ -51,4 +53,58 @@
         json = json.replace("%", "0")
         Json.parseToJsonElement(json)
     }
+
+    private open class NullAsElementSerializer<T : Any>(private val serializer: KSerializer<T>, val nullElement: T) : KSerializer<T?> {
+        final override val descriptor: SerialDescriptor = serializer.descriptor.nullable
+
+        final override fun serialize(encoder: Encoder, value: T?) {
+            serializer.serialize(encoder, value ?: nullElement)
+        }
+
+        final override fun deserialize(decoder: Decoder): T = serializer.deserialize(decoder)
+    }
+
+    private object NullAsJsonNullJsonElementSerializer : NullAsElementSerializer<JsonElement>(JsonElement.serializer(), JsonNull)
+    private object NullAsJsonNullJsonPrimitiveSerializer : NullAsElementSerializer<JsonPrimitive>(JsonPrimitive.serializer(), JsonNull)
+    private object NullAsJsonNullJsonNullSerializer : NullAsElementSerializer<JsonNull>(JsonNull.serializer(), JsonNull)
+    private val noExplicitNullsOrDefaultsJson = Json {
+        explicitNulls = false
+        encodeDefaults = false
+    }
+
+    @Test
+    fun testNullableJsonElementDecoding() {
+        @Serializable
+        data class Wrapper(
+            @Serializable(NullAsJsonNullJsonElementSerializer::class)
+            val value: JsonElement? = null,
+        )
+
+        assertJsonFormAndRestored(Wrapper.serializer(), Wrapper(value = JsonNull), """{"value":null}""", noExplicitNullsOrDefaultsJson)
+        assertJsonFormAndRestored(Wrapper.serializer(), Wrapper(value = null), """{}""", noExplicitNullsOrDefaultsJson)
+    }
+
+    @Test
+    fun testNullableJsonPrimitiveDecoding() {
+        @Serializable
+        data class Wrapper(
+            @Serializable(NullAsJsonNullJsonPrimitiveSerializer::class)
+            val value: JsonPrimitive? = null,
+        )
+
+        assertJsonFormAndRestored(Wrapper.serializer(), Wrapper(value = JsonNull), """{"value":null}""", noExplicitNullsOrDefaultsJson)
+        assertJsonFormAndRestored(Wrapper.serializer(), Wrapper(value = null), """{}""", noExplicitNullsOrDefaultsJson)
+    }
+
+    @Test
+    fun testNullableJsonNullDecoding() {
+        @Serializable
+        data class Wrapper(
+            @Serializable(NullAsJsonNullJsonNullSerializer::class)
+            val value: JsonNull? = null,
+        )
+
+        assertJsonFormAndRestored(Wrapper.serializer(), Wrapper(value = JsonNull), """{"value":null}""", noExplicitNullsOrDefaultsJson)
+        assertJsonFormAndRestored(Wrapper.serializer(), Wrapper(value = null), """{}""", noExplicitNullsOrDefaultsJson)
+    }
 }
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonErrorMessagesTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonErrorMessagesTest.kt
index 8c16ac0..08d1eef 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonErrorMessagesTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonErrorMessagesTest.kt
@@ -6,6 +6,7 @@
 package kotlinx.serialization.json
 
 import kotlinx.serialization.*
+import kotlinx.serialization.test.*
 import kotlin.test.*
 
 
@@ -155,11 +156,4 @@
         })
 
     }
-
-    private fun checkSerializationException(action: () -> Unit, assertions: SerializationException.(String) -> Unit) {
-        val e = assertFailsWith(SerializationException::class, action)
-        assertNotNull(e.message)
-        e.assertions(e.message!!)
-    }
-
 }
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonMapKeysTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonMapKeysTest.kt
index 61f31de..560e51f 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonMapKeysTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonMapKeysTest.kt
@@ -5,6 +5,7 @@
 package kotlinx.serialization.json
 
 import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
 import kotlinx.serialization.descriptors.PrimitiveKind
 import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
 import kotlinx.serialization.descriptors.SerialDescriptor
@@ -42,6 +43,9 @@
     private data class WithMap(val map: Map<Long, Long>)
 
     @Serializable
+    private data class WithBooleanMap(val map: Map<Boolean, Boolean>)
+
+    @Serializable
     private data class WithValueKeyMap(val map: Map<PrimitiveCarrier, Long>)
 
     @Serializable
@@ -60,16 +64,45 @@
     private data class WithContextualKey(val map: Map<@Contextual ContextualValue, Long>)
 
     @Test
-    fun testMapKeysShouldBeStrings() = parametrizedTest(default) {
+    fun testMapKeysSupportNumbers() = parametrizedTest {
         assertStringFormAndRestored(
             """{"map":{"10":10,"20":20}}""",
             WithMap(mapOf(10L to 10L, 20L to 20L)),
             WithMap.serializer(),
-            this
+            default
         )
     }
 
     @Test
+    fun testMapKeysSupportBooleans() = parametrizedTest {
+        assertStringFormAndRestored(
+            """{"map":{"true":false,"false":true}}""",
+            WithBooleanMap(mapOf(true to false, false to true)),
+            WithBooleanMap.serializer(),
+            default
+        )
+    }
+
+    // As a result of quoting ignorance when parsing primitives, it is possible to parse unquoted maps if Kotlin keys are non-string primitives.
+    // This is not spec-compliant, but I do not see any problems with it.
+    @Test
+    fun testMapDeserializesUnquotedKeys() = parametrizedTest {
+        assertEquals(WithMap(mapOf(10L to 10L, 20L to 20L)), default.decodeFromString("""{"map":{10:10,20:20}}"""))
+        assertEquals(
+            WithBooleanMap(mapOf(true to false, false to true)),
+            default.decodeFromString("""{"map":{true:false,false:true}}""")
+        )
+        assertFailsWithSerial("JsonDecodingException") {
+            default.decodeFromString(
+                MapSerializer(
+                    String.serializer(),
+                    Boolean.serializer()
+                ),"""{"map":{true:false,false:true}}"""
+            )
+        }
+    }
+
+    @Test
     fun testStructuredMapKeysShouldBeProhibitedByDefault() = parametrizedTest { streaming ->
         verifyProhibition(WithComplexKey(mapOf(IntData(42) to "42")), streaming)
         verifyProhibition(WithComplexValueKey(mapOf(ComplexCarrier(IntData(42)) to "42")), streaming)
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonParserTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonParserTest.kt
index 7f9044f..94f7052 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonParserTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonParserTest.kt
@@ -83,7 +83,7 @@
     }
 
     private fun testTrailingComma(content: String) {
-        assertFailsWithSerialMessage("JsonDecodingException", "Unexpected trailing") {  Json.parseToJsonElement(content) }
+        assertFailsWithSerialMessage("JsonDecodingException", "Trailing comma before the end of JSON object") {  Json.parseToJsonElement(content) }
     }
 
     @Test
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt
index ebcc3d0..6f3b132 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt
@@ -49,7 +49,7 @@
                 encodeViaStream(serializer, value)
             }
             JsonTestingMode.TREE -> {
-                val tree = writeJson(value, serializer)
+                val tree = writeJson(this, value, serializer)
                 encodeToString(tree)
             }
             JsonTestingMode.OKIO_STREAMS -> {
@@ -77,8 +77,8 @@
                 decodeViaStream(deserializer, source)
             }
             JsonTestingMode.TREE -> {
-                val tree = decodeStringToJsonTree(deserializer, source)
-                readJson(tree, deserializer)
+                val tree = decodeStringToJsonTree(this, deserializer, source)
+                readJson(this, tree, deserializer)
             }
             JsonTestingMode.OKIO_STREAMS -> {
                 val buffer = Buffer()
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/LenientTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/LenientTest.kt
index 41d3c2c..d24c7d7 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/LenientTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/LenientTest.kt
@@ -37,7 +37,7 @@
     @Test
     fun testQuotedBoolean() = parametrizedTest {
         val json = """{"i":1, "l":2, "b":"true", "s":"string"}"""
-        assertFailsWithSerial("JsonDecodingException") { default.decodeFromString(Holder.serializer(), json, it) }
+        assertEquals(value, default.decodeFromString(Holder.serializer(), json, it))
         assertEquals(value, lenient.decodeFromString(Holder.serializer(), json, it))
     }
 
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/TrailingCommaTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/TrailingCommaTest.kt
new file mode 100644
index 0000000..0916de5
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/TrailingCommaTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.json
+
+import kotlinx.serialization.*
+import kotlinx.serialization.test.*
+import kotlin.test.*
+
+class TrailingCommaTest : JsonTestBase() {
+    val tj = Json { allowTrailingComma = true }
+
+    @Serializable
+    data class Optional(val data: String = "")
+
+    @Serializable
+    data class MultipleFields(val a: String, val b: String, val c: String)
+
+    private val multipleFields = MultipleFields("1", "2", "3")
+
+    @Serializable
+    data class WithMap(val m: Map<String, String>)
+
+    private val withMap = WithMap(mapOf("a" to "1", "b" to "2", "c" to "3"))
+
+    @Serializable
+    data class WithList(val l: List<Int>)
+
+    private val withList = WithList(listOf(1, 2, 3))
+
+    @Test
+    fun basic() = parametrizedTest { mode ->
+        val sd = """{"data":"str",}"""
+        assertEquals(Optional("str"), tj.decodeFromString<Optional>(sd, mode))
+    }
+
+    @Test
+    fun trailingCommaNotAllowedByDefaultForObjects() = parametrizedTest { mode ->
+        val sd = """{"data":"str",}"""
+        checkSerializationException({
+            default.decodeFromString<Optional>(sd, mode)
+        }, { message ->
+            assertContains(
+                message,
+                """Unexpected JSON token at offset 13: Trailing comma before the end of JSON object"""
+            )
+        })
+    }
+
+    @Test
+    fun trailingCommaNotAllowedByDefaultForLists() = parametrizedTest { mode ->
+        val sd = """{"l":[1,]}"""
+        checkSerializationException({
+            default.decodeFromString<WithList>(sd, mode)
+        }, { message ->
+            assertContains(
+                message,
+                """Unexpected JSON token at offset 7: Trailing comma before the end of JSON array"""
+            )
+        })
+    }
+
+    @Test
+    fun trailingCommaNotAllowedByDefaultForMaps() = parametrizedTest { mode ->
+        val sd = """{"m":{"a": "b",}}"""
+        checkSerializationException({
+            default.decodeFromString<WithMap>(sd, mode)
+        }, { message ->
+            assertContains(
+                message,
+                """Unexpected JSON token at offset 14: Trailing comma before the end of JSON object"""
+            )
+        })
+    }
+
+    @Test
+    fun emptyObjectNotAllowed() = parametrizedTest { mode ->
+        assertFailsWithMessage<SerializationException>("Unexpected leading comma") {
+            tj.decodeFromString<Optional>("""{,}""", mode)
+        }
+    }
+
+    @Test
+    fun emptyListNotAllowed() = parametrizedTest { mode ->
+        assertFailsWithMessage<SerializationException>("Unexpected leading comma") {
+            tj.decodeFromString<WithList>("""{"l":[,]}""", mode)
+        }
+    }
+
+    @Test
+    fun emptyMapNotAllowed() = parametrizedTest { mode ->
+        assertFailsWithMessage<SerializationException>("Unexpected leading comma") {
+            tj.decodeFromString<WithMap>("""{"m":{,}}""", mode)
+        }
+    }
+
+    @Test
+    fun testMultipleFields() = parametrizedTest { mode ->
+        val input = """{"a":"1","b":"2","c":"3", }"""
+        assertEquals(multipleFields, tj.decodeFromString(input, mode))
+    }
+
+    @Test
+    fun testWithMap() = parametrizedTest { mode ->
+        val input = """{"m":{"a":"1","b":"2","c":"3", }}"""
+
+        assertEquals(withMap, tj.decodeFromString(input, mode))
+    }
+
+    @Test
+    fun testWithList() = parametrizedTest { mode ->
+        val input = """{"l":[1, 2, 3, ]}"""
+        assertEquals(withList, tj.decodeFromString(input, mode))
+    }
+
+    @Serializable
+    data class Mixed(val mf: MultipleFields, val wm: WithMap, val wl: WithList)
+
+    @Test
+    fun testMixed() = parametrizedTest { mode ->
+        //language=JSON5
+        val input = """{"mf":{"a":"1","b":"2","c":"3",},
+            "wm":{"m":{"a":"1","b":"2","c":"3",},},
+            "wl":{"l":[1, 2, 3,],},}"""
+        assertEquals(Mixed(multipleFields, withMap, withList), tj.decodeFromString(input, mode))
+    }
+}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
index 92cf087..8c3633b 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
@@ -5,7 +5,7 @@
 package kotlinx.serialization.test
 
 enum class Platform {
-    JVM, JS, NATIVE
+    JVM, JS, NATIVE, WASM
 }
 
 public expect val currentPlatform: Platform
@@ -13,3 +13,4 @@
 public fun isJs(): Boolean = currentPlatform == Platform.JS
 public fun isJvm(): Boolean = currentPlatform == Platform.JVM
 public fun isNative(): Boolean = currentPlatform == Platform.NATIVE
+public fun isWasm(): Boolean = currentPlatform == Platform.WASM
\ No newline at end of file
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt
index e941f04..3ec0714 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt
@@ -92,3 +92,9 @@
         "expected:<$message> but was:<${exception.message}>"
     )
 }
+
+inline fun checkSerializationException(action: () -> Unit, assertions: SerializationException.(String) -> Unit) {
+    val e = assertFailsWith(SerializationException::class, action)
+    assertNotNull(e.message)
+    e.assertions(e.message!!)
+}
diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/JavaCollectionsTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/JavaCollectionsTest.kt
index a0ff55a..1f4958a 100644
--- a/formats/json-tests/jvmTest/src/kotlinx/serialization/JavaCollectionsTest.kt
+++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/JavaCollectionsTest.kt
@@ -17,6 +17,7 @@
 package kotlinx.serialization
 
 import kotlinx.serialization.json.Json
+import kotlinx.serialization.test.typeTokenOf
 import org.junit.Test
 import java.util.HashMap
 import java.util.HashSet
@@ -24,7 +25,8 @@
 import kotlin.collections.Map
 import kotlin.collections.hashMapOf
 import kotlin.collections.hashSetOf
-import kotlin.test.assertEquals
+import kotlin.reflect.*
+import kotlin.test.*
 
 
 class JavaCollectionsTest {
@@ -38,7 +40,7 @@
     )
 
     @Test
-    fun test() {
+    fun testJavaCollectionsInsideClass() {
         val original = HasHashMap("42", hashMapOf(1 to "1", 2 to "2"), hashSetOf(11), LinkedHashMap(), null)
         val serializer = HasHashMap.serializer()
         val string = Json.encodeToString(serializer = serializer, value = original)
@@ -49,4 +51,62 @@
         val restored = Json.decodeFromString(deserializer = serializer, string = string)
         assertEquals(expected = original, actual = restored)
     }
+
+    @Test
+    fun testTopLevelMaps() {
+        // Returning null here is a deliberate choice: map constructor functions may return different specialized
+        // implementations (e.g., kotlin.collections.EmptyMap or java.util.Collections.SingletonMap)
+        // that may or may not be generic. Since we generally cannot return a generic serializer using Java class only,
+        // all attempts to get map serializer using only .javaClass should return null.
+        assertNull(serializerOrNull(emptyMap<String, String>().javaClass))
+        assertNull(serializerOrNull(mapOf<String, String>("a" to "b").javaClass))
+        assertNull(serializerOrNull(mapOf<String, String>("a" to "b", "b" to "c").javaClass))
+        // Correct ways of retrieving map serializer:
+        assertContains(
+            serializer(typeTokenOf<Map<String, String>>()).descriptor.serialName,
+            "kotlin.collections.LinkedHashMap"
+        )
+        assertContains(
+            serializer(typeTokenOf<java.util.LinkedHashMap<String, String>>()).descriptor.serialName,
+            "kotlin.collections.LinkedHashMap"
+        )
+        assertContains(
+            serializer(typeOf<LinkedHashMap<String, String>>()).descriptor.serialName,
+            "kotlin.collections.LinkedHashMap"
+        )
+    }
+
+    @Test
+    fun testTopLevelSetsAndLists() {
+        // Same reasoning as for maps
+        assertNull(serializerOrNull(emptyList<String>().javaClass))
+        assertNull(serializerOrNull(listOf<String>("a").javaClass))
+        assertNull(serializerOrNull(listOf<String>("a", "b").javaClass))
+        assertNull(serializerOrNull(emptySet<String>().javaClass))
+        assertNull(serializerOrNull(setOf<String>("a").javaClass))
+        assertNull(serializerOrNull(setOf<String>("a", "b").javaClass))
+        assertContains(
+            serializer(typeTokenOf<Set<String>>()).descriptor.serialName,
+            "kotlin.collections.LinkedHashSet"
+        )
+        assertContains(
+            serializer(typeTokenOf<List<String>>()).descriptor.serialName,
+            "kotlin.collections.ArrayList"
+        )
+        assertContains(
+            serializer(typeTokenOf<java.util.LinkedHashSet<String>>()).descriptor.serialName,
+            "kotlin.collections.LinkedHashSet"
+        )
+        assertContains(
+            serializer(typeTokenOf<java.util.ArrayList<String>>()).descriptor.serialName,
+            "kotlin.collections.ArrayList"
+        )
+    }
+
+    @Test
+    fun testAnonymousObject() {
+        val obj: Any = object {}
+        assertNull(serializerOrNull(obj.javaClass))
+    }
 }
+
diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt
index 3bc4528..a600b9d 100644
--- a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt
+++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt
@@ -209,15 +209,6 @@
         assertEquals(expected, json.encodeToString(serial2 as KSerializer<T>, value))
     }
 
-    @PublishedApi
-    internal open class TypeBase<T>
-
-    public inline fun <reified T> typeTokenOf(): Type {
-        val base = object : TypeBase<T>() {}
-        val superType = base::class.java.genericSuperclass!!
-        return (superType as ParameterizedType).actualTypeArguments.first()!!
-    }
-
     class IntBox(val i: Int)
 
     object CustomIntSerializer : KSerializer<IntBox> {
diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/test/TypeToken.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/TypeToken.kt
new file mode 100644
index 0000000..2b04274
--- /dev/null
+++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/TypeToken.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.test
+
+import java.lang.reflect.*
+
+
+@PublishedApi
+internal open class TypeBase<T>
+
+public inline fun <reified T> typeTokenOf(): Type {
+    val base = object : TypeBase<T>() {}
+    val superType = base::class.java.genericSuperclass!!
+    return (superType as ParameterizedType).actualTypeArguments.first()!!
+}
diff --git a/formats/json-tests/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt b/formats/json-tests/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt
new file mode 100644
index 0000000..fd359b7
--- /dev/null
+++ b/formats/json-tests/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt
@@ -0,0 +1,7 @@
+/*
+ * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.test
+
+public actual val currentPlatform: Platform = Platform.WASM
\ No newline at end of file
diff --git a/formats/json-tests/wasmTest/src/kotlinx/serialization/test/JsonHelpers.kt b/formats/json-tests/wasmTest/src/kotlinx/serialization/test/JsonHelpers.kt
new file mode 100644
index 0000000..6102e58
--- /dev/null
+++ b/formats/json-tests/wasmTest/src/kotlinx/serialization/test/JsonHelpers.kt
@@ -0,0 +1,19 @@
+package kotlinx.serialization.test
+
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.json.Json
+
+actual fun <T> Json.encodeViaStream(
+    serializer: SerializationStrategy<T>,
+    value: T
+): String {
+    TODO("supported on JVM only")
+}
+
+actual fun <T> Json.decodeViaStream(
+    serializer: DeserializationStrategy<T>,
+    input: String
+): T {
+    TODO("supported on JVM only")
+}
\ No newline at end of file
diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api
index e6a2328..649cce0 100644
--- a/formats/json/api/kotlinx-serialization-json.api
+++ b/formats/json/api/kotlinx-serialization-json.api
@@ -87,6 +87,7 @@
 public final class kotlinx/serialization/json/JsonBuilder {
 	public final fun getAllowSpecialFloatingPointValues ()Z
 	public final fun getAllowStructuredMapKeys ()Z
+	public final fun getAllowTrailingComma ()Z
 	public final fun getClassDiscriminator ()Ljava/lang/String;
 	public final fun getCoerceInputValues ()Z
 	public final fun getDecodeEnumsCaseInsensitive ()Z
@@ -102,6 +103,7 @@
 	public final fun isLenient ()Z
 	public final fun setAllowSpecialFloatingPointValues (Z)V
 	public final fun setAllowStructuredMapKeys (Z)V
+	public final fun setAllowTrailingComma (Z)V
 	public final fun setClassDiscriminator (Ljava/lang/String;)V
 	public final fun setCoerceInputValues (Z)V
 	public final fun setDecodeEnumsCaseInsensitive (Z)V
@@ -130,6 +132,7 @@
 	public fun <init> ()V
 	public final fun getAllowSpecialFloatingPointValues ()Z
 	public final fun getAllowStructuredMapKeys ()Z
+	public final fun getAllowTrailingComma ()Z
 	public final fun getClassDiscriminator ()Ljava/lang/String;
 	public final fun getCoerceInputValues ()Z
 	public final fun getDecodeEnumsCaseInsensitive ()Z
@@ -383,14 +386,11 @@
 	public static final fun encodeToStream (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Ljava/io/OutputStream;)V
 }
 
-public final class kotlinx/serialization/json/internal/JsonStreamsKt {
-	public static final fun decodeByReader (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/internal/SerialReader;)Ljava/lang/Object;
-	public static final fun decodeToSequenceByReader (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/json/internal/SerialReader;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/DecodeSequenceMode;)Lkotlin/sequences/Sequence;
-	public static synthetic fun decodeToSequenceByReader$default (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/json/internal/SerialReader;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/DecodeSequenceMode;ILjava/lang/Object;)Lkotlin/sequences/Sequence;
-	public static final fun encodeByWriter (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/json/internal/JsonWriter;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V
+public abstract interface class kotlinx/serialization/json/internal/InternalJsonReader {
+	public abstract fun read ([CII)I
 }
 
-public abstract interface class kotlinx/serialization/json/internal/JsonWriter {
+public abstract interface class kotlinx/serialization/json/internal/InternalJsonWriter {
 	public abstract fun release ()V
 	public abstract fun write (Ljava/lang/String;)V
 	public abstract fun writeChar (C)V
@@ -398,18 +398,17 @@
 	public abstract fun writeQuoted (Ljava/lang/String;)V
 }
 
-public abstract interface class kotlinx/serialization/json/internal/SerialReader {
-	public abstract fun read ([CII)I
+public final class kotlinx/serialization/json/internal/JsonStreamsKt {
+	public static final fun decodeByReader (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/internal/InternalJsonReader;)Ljava/lang/Object;
+	public static final fun decodeToSequenceByReader (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/json/internal/InternalJsonReader;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/DecodeSequenceMode;)Lkotlin/sequences/Sequence;
+	public static synthetic fun decodeToSequenceByReader$default (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/json/internal/InternalJsonReader;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/DecodeSequenceMode;ILjava/lang/Object;)Lkotlin/sequences/Sequence;
+	public static final fun encodeByWriter (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/json/internal/InternalJsonWriter;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V
 }
 
 public final class kotlinx/serialization/json/internal/StreamingJsonDecoderKt {
 	public static final fun decodeStringToJsonTree (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/DeserializationStrategy;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement;
 }
 
-public final class kotlinx/serialization/json/internal/StringOpsKt {
-	public static final fun getESCAPE_STRINGS ()[Ljava/lang/String;
-}
-
 public final class kotlinx/serialization/json/internal/TreeJsonDecoderKt {
 	public static final fun readJson (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object;
 }
diff --git a/formats/json/build.gradle b/formats/json/build.gradle
index 8f2b5af..a71366f 100644
--- a/formats/json/build.gradle
+++ b/formats/json/build.gradle
@@ -1,3 +1,5 @@
+import static KotlinVersion.isKotlinVersionAtLeast
+
 /*
  * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
@@ -19,14 +21,35 @@
 }
 
 kotlin {
-
     sourceSets {
+        configureEach {
+            languageSettings {
+                optIn("kotlinx.serialization.internal.CoreFriendModuleApi")
+                optIn("kotlinx.serialization.json.internal.JsonFriendModuleApi")
+            }
+        }
         commonMain {
             dependencies {
                 api project(":kotlinx-serialization-core")
             }
         }
+        jsWasmMain {
+            dependsOn(sourceSets.commonMain)
+        }
+        jsMain {
+            dependsOn(sourceSets.jsWasmMain)
+        }
+        wasmJsMain {
+            dependsOn(sourceSets.jsWasmMain)
+        }
     }
 }
 
 Java9Modularity.configureJava9ModuleInfo(project)
+
+// This task should be disabled because of no need to build and publish intermediate JsWasm sourceset
+tasks.whenTaskAdded { task ->
+    if (task.name == 'compileJsWasmMainKotlinMetadata') {
+        task.enabled = false
+    }
+}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
index 40dcc23..26c376e 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
@@ -78,7 +78,7 @@
     public final override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
         val result = JsonToStringWriter()
         try {
-            encodeByWriter(result, serializer, value)
+            encodeByWriter(this@Json, result, serializer, value)
             return result.toString()
         } finally {
             result.release()
@@ -114,7 +114,7 @@
      * @throws [SerializationException] if the given value cannot be serialized to JSON
      */
     public fun <T> encodeToJsonElement(serializer: SerializationStrategy<T>, value: T): JsonElement {
-        return writeJson(value, serializer)
+        return writeJson(this@Json, value, serializer)
     }
 
     /**
@@ -124,7 +124,7 @@
      * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
      */
     public fun <T> decodeFromJsonElement(deserializer: DeserializationStrategy<T>, element: JsonElement): T {
-        return readJson(element, deserializer)
+        return readJson(this@Json, element, deserializer)
     }
 
     /**
@@ -365,6 +365,16 @@
     public var decodeEnumsCaseInsensitive: Boolean = json.configuration.decodeEnumsCaseInsensitive
 
     /**
+     * Allows parser to accept trailing (ending) commas in JSON objects and arrays,
+     * making inputs like `[1, 2, 3,]` valid.
+     *
+     * Does not affect encoding.
+     * `false` by default.
+     */
+    @ExperimentalSerializationApi
+    public var allowTrailingComma: Boolean = json.configuration.allowTrailingComma
+
+    /**
      * Module with contextual and polymorphic serializers to be used in the resulting [Json] instance.
      *
      * @see SerializersModule
@@ -396,7 +406,7 @@
             allowStructuredMapKeys, prettyPrint, explicitNulls, prettyPrintIndent,
             coerceInputValues, useArrayPolymorphism,
             classDiscriminator, allowSpecialFloatingPointValues, useAlternativeNames,
-            namingStrategy, decodeEnumsCaseInsensitive
+            namingStrategy, decodeEnumsCaseInsensitive, allowTrailingComma
         )
     }
 }
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
index ea653a6..053f4cd 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
@@ -32,7 +32,9 @@
     @ExperimentalSerializationApi
     public val namingStrategy: JsonNamingStrategy? = null,
     @ExperimentalSerializationApi
-    public val decodeEnumsCaseInsensitive: Boolean = false
+    public val decodeEnumsCaseInsensitive: Boolean = false,
+    @ExperimentalSerializationApi
+    public val allowTrailingComma: Boolean = false,
 ) {
 
     /** @suppress Dokka **/
@@ -42,6 +44,6 @@
                 "allowStructuredMapKeys=$allowStructuredMapKeys, prettyPrint=$prettyPrint, explicitNulls=$explicitNulls, " +
                 "prettyPrintIndent='$prettyPrintIndent', coerceInputValues=$coerceInputValues, useArrayPolymorphism=$useArrayPolymorphism, " +
                 "classDiscriminator='$classDiscriminator', allowSpecialFloatingPointValues=$allowSpecialFloatingPointValues, useAlternativeNames=$useAlternativeNames, " +
-                "namingStrategy=$namingStrategy, decodeEnumsCaseInsensitive=$decodeEnumsCaseInsensitive)"
+                "namingStrategy=$namingStrategy, decodeEnumsCaseInsensitive=$decodeEnumsCaseInsensitive, allowTrailingComma=$allowTrailingComma)"
     }
 }
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt
index 8d1485d..22d87a7 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt
@@ -67,7 +67,7 @@
 
     final override fun serialize(encoder: Encoder, value: T) {
         val output = encoder.asJsonEncoder()
-        var element = output.json.writeJson(value, tSerializer)
+        var element = writeJson(output.json, value, tSerializer)
         element = transformSerialize(element)
         output.encodeJsonElement(element)
     }
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt
index d584155..c1ed8cc 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt
@@ -9,11 +9,11 @@
 import kotlinx.serialization.json.*
 import kotlin.jvm.*
 
-internal fun Composer(sb: JsonWriter, json: Json): Composer =
+internal fun Composer(sb: InternalJsonWriter, json: Json): Composer =
     if (json.configuration.prettyPrint) ComposerWithPrettyPrint(sb, json) else Composer(sb)
 
 @OptIn(ExperimentalSerializationApi::class)
-internal open class Composer(@JvmField internal val writer: JsonWriter) {
+internal open class Composer(@JvmField internal val writer: InternalJsonWriter) {
     var writingFirst = true
         protected set
 
@@ -42,7 +42,7 @@
 }
 
 @SuppressAnimalSniffer // Long(Integer).toUnsignedString(long)
-internal class ComposerForUnsignedNumbers(writer: JsonWriter, private val forceQuoting: Boolean) : Composer(writer) {
+internal class ComposerForUnsignedNumbers(writer: InternalJsonWriter, private val forceQuoting: Boolean) : Composer(writer) {
     override fun print(v: Int) {
         if (forceQuoting) printQuoted(v.toUInt().toString()) else print(v.toUInt().toString())
     }
@@ -61,14 +61,14 @@
 }
 
 @SuppressAnimalSniffer
-internal class ComposerForUnquotedLiterals(writer: JsonWriter, private val forceQuoting: Boolean) : Composer(writer) {
+internal class ComposerForUnquotedLiterals(writer: InternalJsonWriter, private val forceQuoting: Boolean) : Composer(writer) {
     override fun printQuoted(value: String) {
         if (forceQuoting) super.printQuoted(value) else super.print(value)
     }
 }
 
 internal class ComposerWithPrettyPrint(
-    writer: JsonWriter,
+    writer: InternalJsonWriter,
     private val json: Json
 ) : Composer(writer) {
     private var level = 0
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonElementMarker.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonElementMarker.kt
index 2535739..85a4624 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonElementMarker.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonElementMarker.kt
@@ -2,7 +2,6 @@
  * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
-@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
 
 package kotlinx.serialization.json.internal
 
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt
index ad5011a..c6098dd 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt
@@ -46,6 +46,13 @@
         hint = specialFlowingValuesHint)
 }
 
+internal fun AbstractJsonLexer.invalidTrailingComma(entity: String = "object"): Nothing {
+    fail("Trailing comma before the end of JSON $entity",
+        position = currentPosition - 1,
+        hint = "Trailing commas are non-complaint JSON and not allowed by default. Use 'allowTrailingCommas = true' in 'Json {}' builder to support them."
+    )
+}
+
 @OptIn(ExperimentalSerializationApi::class)
 internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEncodingException(
     "Value of type '${keyDescriptor.serialName}' can't be used in JSON as a key in the map. " +
@@ -75,7 +82,7 @@
             "Current input: ${input.minify()}"
 )
 
-private fun CharSequence.minify(offset: Int = -1): CharSequence {
+internal fun CharSequence.minify(offset: Int = -1): CharSequence {
     if (length < 200) return this
     if (offset == -1) {
         val start = this.length - 60
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt
index 74dbe45..05f025c 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt
@@ -4,38 +4,42 @@
 import kotlinx.serialization.json.DecodeSequenceMode
 import kotlinx.serialization.json.Json
 
-@PublishedApi
-internal interface JsonWriter {
-    fun writeLong(value: Long)
-    fun writeChar(char: Char)
-    fun write(text: String)
-    fun writeQuoted(text: String)
-    fun release()
+@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
+internal annotation class JsonFriendModuleApi
+
+@JsonFriendModuleApi
+public interface InternalJsonWriter {
+    public fun writeLong(value: Long)
+    public fun writeChar(char: Char)
+    public fun write(text: String)
+    public fun writeQuoted(text: String)
+    public fun release()
 }
 
-@PublishedApi
-internal interface SerialReader {
-    fun read(buffer: CharArray, bufferOffset: Int, count: Int): Int
+@JsonFriendModuleApi
+public interface InternalJsonReader {
+    public fun read(buffer: CharArray, bufferOffset: Int, count: Int): Int
 }
 
-@PublishedApi
-internal fun <T> Json.encodeByWriter(writer: JsonWriter, serializer: SerializationStrategy<T>, value: T) {
+@JsonFriendModuleApi
+public fun <T> encodeByWriter(json: Json, writer: InternalJsonWriter, serializer: SerializationStrategy<T>, value: T) {
     val encoder = StreamingJsonEncoder(
-        writer, this,
+        writer, json,
         WriteMode.OBJ,
-        arrayOfNulls(WriteMode.values().size)
+        arrayOfNulls(WriteMode.entries.size)
     )
     encoder.encodeSerializableValue(serializer, value)
 }
 
-@PublishedApi
-internal fun <T> Json.decodeByReader(
+@JsonFriendModuleApi
+public fun <T> decodeByReader(
+    json: Json,
     deserializer: DeserializationStrategy<T>,
-    reader: SerialReader
+    reader: InternalJsonReader
 ): T {
     val lexer = ReaderJsonLexer(reader)
     try {
-        val input = StreamingJsonDecoder(this, WriteMode.OBJ, lexer, deserializer.descriptor, null)
+        val input = StreamingJsonDecoder(json, WriteMode.OBJ, lexer, deserializer.descriptor, null)
         val result = input.decodeSerializableValue(deserializer)
         lexer.expectEof()
         return result
@@ -44,21 +48,23 @@
     }
 }
 
-@PublishedApi
+@JsonFriendModuleApi
 @ExperimentalSerializationApi
-internal fun <T> Json.decodeToSequenceByReader(
-    reader: SerialReader,
+public fun <T> decodeToSequenceByReader(
+    json: Json,
+    reader: InternalJsonReader,
     deserializer: DeserializationStrategy<T>,
     format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
 ): Sequence<T> {
     val lexer = ReaderJsonLexer(reader, CharArray(BATCH_SIZE)) // Unpooled buffer due to lazy nature of sequence
-    val iter = JsonIterator(format, this, lexer, deserializer)
+    val iter = JsonIterator(format, json, lexer, deserializer)
     return Sequence { iter }.constrainOnce()
 }
 
-@PublishedApi
+@JsonFriendModuleApi
 @ExperimentalSerializationApi
-internal inline fun <reified T> Json.decodeToSequenceByReader(
-    reader: SerialReader,
+public inline fun <reified T> decodeToSequenceByReader(
+    json: Json,
+    reader: InternalJsonReader,
     format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
-): Sequence<T> = decodeToSequenceByReader(reader, serializersModule.serializer(), format)
+): Sequence<T> = decodeToSequenceByReader(json, reader, json.serializersModule.serializer(), format)
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
index 4df35de..a6a123c 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
@@ -1,6 +1,6 @@
 package kotlinx.serialization.json.internal
 
-internal expect class JsonToStringWriter constructor() : JsonWriter {
+internal expect class JsonToStringWriter constructor() : InternalJsonWriter {
     override fun writeChar(char: Char)
     override fun writeLong(value: Long)
     override fun write(text: String)
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonTreeReader.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonTreeReader.kt
index 060c36b..9cb9bb3 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonTreeReader.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonTreeReader.kt
@@ -13,6 +13,7 @@
     private val lexer: AbstractJsonLexer
 ) {
     private val isLenient = configuration.isLenient
+    private val trailingCommaAllowed = configuration.allowTrailingComma
     private var stackDepth = 0
 
     private fun readObject(): JsonElement = readObjectImpl {
@@ -44,8 +45,9 @@
         if (lastToken == TC_BEGIN_OBJ) { // Case of empty object
             lexer.consumeNextToken(TC_END_OBJ)
         } else if (lastToken == TC_COMMA) { // Trailing comma
-            lexer.fail("Unexpected trailing comma")
-        }
+            if (!trailingCommaAllowed) lexer.invalidTrailingComma()
+            lexer.consumeNextToken(TC_END_OBJ)
+        } // else unexpected token?
         return JsonObject(result)
     }
 
@@ -66,7 +68,8 @@
         if (lastToken == TC_BEGIN_LIST) { // Case of empty object
             lexer.consumeNextToken(TC_END_LIST)
         } else if (lastToken == TC_COMMA) { // Trailing comma
-            lexer.fail("Unexpected trailing comma")
+            if (!trailingCommaAllowed) lexer.invalidTrailingComma("array")
+            lexer.consumeNextToken(TC_END_LIST)
         }
         return JsonArray(result)
     }
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 c1c9126..bd658fc 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
@@ -63,20 +63,15 @@
     val discriminator = deserializer.descriptor.classDiscriminator(json)
 
     val jsonTree = cast<JsonObject>(decodeJsonElement(), deserializer.descriptor)
-    val type = jsonTree[discriminator]?.jsonPrimitive?.content
-    val actualSerializer = deserializer.findPolymorphicSerializerOrNull(this, type)
-        ?: throwSerializerNotFound(type, jsonTree)
-
+    val type = jsonTree[discriminator]?.jsonPrimitive?.contentOrNull // differentiate between `"type":"null"` and `"type":null`.
     @Suppress("UNCHECKED_CAST")
-    return json.readPolymorphicJson(discriminator, jsonTree, actualSerializer as DeserializationStrategy<T>)
-}
-
-@JvmName("throwSerializerNotFound")
-internal fun throwSerializerNotFound(type: String?, jsonTree: JsonObject): Nothing {
-    val suffix =
-        if (type == null) "missing class discriminator ('null')"
-        else "class discriminator '$type'"
-    throw JsonDecodingException(-1, "Polymorphic serializer was not found for $suffix", jsonTree.toString())
+    val actualSerializer =
+        try {
+            deserializer.findPolymorphicSerializer(this, type)
+        } catch (it: SerializationException) { //  Wrap SerializationException into JsonDecodingException to preserve input
+            throw JsonDecodingException(-1, it.message!!, jsonTree.toString())
+        } as DeserializationStrategy<T>
+    return json.readPolymorphicJson(discriminator, jsonTree, actualSerializer)
 }
 
 internal fun SerialDescriptor.classDiscriminator(json: Json): String {
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
index 6cedabc..0018fce 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
@@ -49,7 +49,6 @@
 
     override fun decodeJsonElement(): JsonElement = JsonTreeReader(json.configuration, lexer).read()
 
-    @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
     override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
         try {
             /*
@@ -72,19 +71,22 @@
 
             val discriminator = deserializer.descriptor.classDiscriminator(json)
             val type = lexer.peekLeadingMatchingValue(discriminator, configuration.isLenient)
-            var actualSerializer: DeserializationStrategy<Any>? = null
-            if (type != null) {
-                actualSerializer = deserializer.findPolymorphicSerializerOrNull(this, type)
-            }
-            if (actualSerializer == null) {
-                // Fallback if we haven't found discriminator or serializer
+                ?: // Fallback to slow path if we haven't found discriminator on first try
                 return decodeSerializableValuePolymorphic<T>(deserializer as DeserializationStrategy<T>)
-            }
+
+            @Suppress("UNCHECKED_CAST")
+            val actualSerializer = try {
+                    deserializer.findPolymorphicSerializer(this, type)
+                } catch (it: SerializationException) { // Wrap SerializationException into JsonDecodingException to preserve position, path, and input.
+                    // Split multiline message from private core function:
+                    // core/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt:102
+                    val message = it.message!!.substringBefore('\n').removeSuffix(".")
+                    val hint = it.message!!.substringAfter('\n', missingDelimiterValue = "")
+                    lexer.fail(message, hint = hint)
+                } as DeserializationStrategy<T>
 
             discriminatorHolder = DiscriminatorHolder(discriminator)
-            @Suppress("UNCHECKED_CAST")
-            val result = actualSerializer.deserialize(this) as T
-            return result
+            return actualSerializer.deserialize(this)
 
         } catch (e: MissingFieldException) {
             // Add "at path" if and only if we've just caught an exception and it hasn't been augmented yet
@@ -123,6 +125,7 @@
         if (json.configuration.ignoreUnknownKeys && descriptor.elementsCount == 0) {
             skipLeftoverElements(descriptor)
         }
+        if (lexer.tryConsumeComma() && !json.configuration.allowTrailingComma) lexer.invalidTrailingComma("")
         // First consume the object so we know it's correct
         lexer.consumeNextToken(mode.end)
         // Then cleanup the path
@@ -196,12 +199,12 @@
 
         return if (lexer.canConsumeValue()) {
             if (decodingKey) {
-                if (currentIndex == -1) lexer.require(!hasComma) { "Unexpected trailing comma" }
+                if (currentIndex == -1) lexer.require(!hasComma) { "Unexpected leading comma" }
                 else lexer.require(hasComma) { "Expected comma after the key-value pair" }
             }
             ++currentIndex
         } else {
-            if (hasComma) lexer.fail("Expected '}', but had ',' instead")
+            if (hasComma && !json.configuration.allowTrailingComma) lexer.invalidTrailingComma()
             CompositeDecoder.DECODE_DONE
         }
     }
@@ -240,7 +243,7 @@
                 hasComma = handleUnknown(key)
             }
         }
-        if (hasComma) lexer.fail("Unexpected trailing comma")
+        if (hasComma && !json.configuration.allowTrailingComma) lexer.invalidTrailingComma()
 
         return elementMarker?.nextUnmarkedIndex() ?: CompositeDecoder.DECODE_DONE
     }
@@ -263,28 +266,19 @@
             if (currentIndex != -1 && !hasComma) lexer.fail("Expected end of the array or comma")
             ++currentIndex
         } else {
-            if (hasComma) lexer.fail("Unexpected trailing comma")
+            if (hasComma && !json.configuration.allowTrailingComma) lexer.invalidTrailingComma("array")
             CompositeDecoder.DECODE_DONE
         }
     }
 
-
+    /*
+     * The primitives are allowed to be quoted and unquoted
+     * to simplify map key parsing and integrations with third-party API.
+     */
     override fun decodeBoolean(): Boolean {
-        /*
-         * We prohibit any boolean literal that is not strictly 'true' or 'false' as it is considered way too
-         * error-prone, but allow quoted literal in relaxed mode for booleans.
-         */
-        return if (configuration.isLenient) {
-            lexer.consumeBooleanLenient()
-        } else {
-            lexer.consumeBoolean()
-        }
+        return lexer.consumeBooleanLenient()
     }
 
-    /*
-     * The rest of the primitives are allowed to be quoted and unquoted
-     * to simplify integrations with third-party API.
-     */
     override fun decodeByte(): Byte {
         val value = lexer.consumeNumericLiteral()
         // Check for overflow
@@ -359,13 +353,14 @@
     }
 }
 
-@InternalSerializationApi
-public fun <T> Json.decodeStringToJsonTree(
+@JsonFriendModuleApi // used in json-tests
+public fun <T> decodeStringToJsonTree(
+    json: Json,
     deserializer: DeserializationStrategy<T>,
     source: String
 ): JsonElement {
     val lexer = StringJsonLexer(source)
-    val input = StreamingJsonDecoder(this, WriteMode.OBJ, lexer, deserializer.descriptor, null)
+    val input = StreamingJsonDecoder(json, WriteMode.OBJ, lexer, deserializer.descriptor, null)
     val tree = input.decodeJsonElement()
     lexer.expectEof()
     return tree
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt
index c6f76cb..4f7b1ec 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt
@@ -10,7 +10,6 @@
 import kotlinx.serialization.encoding.*
 import kotlinx.serialization.json.*
 import kotlinx.serialization.modules.*
-import kotlin.native.concurrent.*
 
 private val unsignedNumberDescriptors = setOf(
     UInt.serializer().descriptor,
@@ -34,7 +33,7 @@
 ) : JsonEncoder, AbstractEncoder() {
 
     internal constructor(
-        output: JsonWriter, json: Json, mode: WriteMode,
+        output: InternalJsonWriter, json: Json, mode: WriteMode,
         modeReuseCache: Array<JsonEncoder?>
     ) : this(Composer(output, json), json, mode, modeReuseCache)
 
@@ -164,7 +163,7 @@
             else                        -> super.encodeInline(descriptor)
         }
 
-    private inline fun <reified T: Composer> composerAs(composerCreator: (writer: JsonWriter, forceQuoting: Boolean) -> T): T {
+    private inline fun <reified T: Composer> composerAs(composerCreator: (writer: InternalJsonWriter, forceQuoting: Boolean) -> T): T {
         // If we're inside encodeInline().encodeSerializableValue, we should preserve the forceQuoting state
         // inside the composer, but not in the encoder (otherwise we'll get into `if (forceQuoting) encodeString(value.toString())` part
         // and unsigned numbers would be encoded incorrectly)
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt
index f488d90..ed76ba0 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt
@@ -12,7 +12,6 @@
     else (d - 10 + 'a'.code).toChar()
 }
 
-@PublishedApi
 internal val ESCAPE_STRINGS: Array<String?> = arrayOfNulls<String>(93).apply {
     for (c in 0..0x1f) {
         val c1 = toHexChar(c shr 12)
@@ -68,4 +67,4 @@
     this.equals("true", ignoreCase = true) -> true
     this.equals("false", ignoreCase = true) -> false
     else -> null
-}
\ No newline at end of file
+}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt
index 40a3e41..aedfb95 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt
@@ -15,12 +15,12 @@
 import kotlinx.serialization.modules.*
 import kotlin.jvm.*
 
-@InternalSerializationApi
-public fun <T> Json.readJson(element: JsonElement, deserializer: DeserializationStrategy<T>): T {
+@JsonFriendModuleApi
+public fun <T> readJson(json: Json, element: JsonElement, deserializer: DeserializationStrategy<T>): T {
     val input = when (element) {
-        is JsonObject -> JsonTreeDecoder(this, element)
-        is JsonArray -> JsonTreeListDecoder(this, element)
-        is JsonLiteral, JsonNull -> JsonPrimitiveDecoder(this, element as JsonPrimitive)
+        is JsonObject -> JsonTreeDecoder(json, element)
+        is JsonArray -> JsonTreeListDecoder(json, element)
+        is JsonLiteral, JsonNull -> JsonPrimitiveDecoder(json, element as JsonPrimitive)
     }
     return input.decodeSerializableValue(deserializer)
 }
@@ -91,16 +91,7 @@
     override fun decodeTaggedNotNullMark(tag: String): Boolean = currentElement(tag) !== JsonNull
 
     override fun decodeTaggedBoolean(tag: String): Boolean {
-        val value = getPrimitiveValue(tag)
-        if (!json.configuration.isLenient) {
-            val literal = value.asLiteral("boolean")
-            if (literal.isString) throw JsonDecodingException(
-                -1, "Boolean literal for key '$tag' should be unquoted.\n$lenientHint", currentObject().toString()
-            )
-        }
-        return value.primitive("boolean") {
-            booleanOrNull ?: throw IllegalArgumentException() /* Will be handled by 'primitive' */
-        }
+        return getPrimitiveValue(tag).primitive("boolean", JsonPrimitive::booleanOrNull)
     }
 
     override fun decodeTaggedByte(tag: String) = getPrimitiveValue(tag).primitive("byte") {
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt
index e42d425..5e3c808 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt
@@ -14,10 +14,10 @@
 import kotlin.collections.set
 import kotlin.jvm.*
 
-@InternalSerializationApi
-public fun <T> Json.writeJson(value: T, serializer: SerializationStrategy<T>): JsonElement {
+@JsonFriendModuleApi
+public fun <T> writeJson(json: Json, value: T, serializer: SerializationStrategy<T>): JsonElement {
     lateinit var result: JsonElement
-    val encoder = JsonTreeEncoder(this) { result = it }
+    val encoder = JsonTreeEncoder(json) { result = it }
     encoder.encodeSerializableValue(serializer, value)
     return result
 }
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
index a1a6a5e..c83bdef 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
@@ -149,7 +149,7 @@
     protected abstract val source: CharSequence
 
     @JvmField
-    protected var currentPosition: Int = 0 // position in source
+    internal var currentPosition: Int = 0 // position in source
 
     @JvmField
     val path = JsonPath()
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/ReaderJsonLexer.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/ReaderJsonLexer.kt
index 3c785f8..24e5b47 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/ReaderJsonLexer.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/ReaderJsonLexer.kt
@@ -2,8 +2,6 @@
  * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
-@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-
 package kotlinx.serialization.json.internal
 
 internal const val BATCH_SIZE: Int = 16 * 1024
@@ -13,7 +11,7 @@
  * For some reason this hand-rolled implementation is faster than
  * fun ArrayAsSequence(s: CharArray): CharSequence = java.nio.CharBuffer.wrap(s, 0, length)
  */
-internal class ArrayAsSequence(private val buffer: CharArray) : CharSequence {
+internal class ArrayAsSequence(internal val buffer: CharArray) : CharSequence {
     override var length: Int = buffer.size
 
     override fun get(index: Int): Char = buffer[index]
@@ -35,7 +33,7 @@
 }
 
 internal class ReaderJsonLexer(
-    private val reader: SerialReader,
+    private val reader: InternalJsonReader,
     private val buffer: CharArray = CharArrayPoolBatchSize.take()
 ) : AbstractJsonLexer() {
     private var threshold: Int = DEFAULT_THRESHOLD // chars
diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/JsonSchemaCache.kt b/formats/json/jsWasmMain/src/kotlinx/serialization/json/JsonSchemaCache.kt
similarity index 100%
rename from formats/json/jsMain/src/kotlinx/serialization/json/JsonSchemaCache.kt
rename to formats/json/jsWasmMain/src/kotlinx/serialization/json/JsonSchemaCache.kt
diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/CharArrayPool.kt b/formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/CharArrayPool.kt
similarity index 100%
rename from formats/json/jsMain/src/kotlinx/serialization/json/internal/CharArrayPool.kt
rename to formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/CharArrayPool.kt
diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/FormatLanguageJs.kt b/formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/FormatLanguageJsWasm.kt
similarity index 100%
rename from formats/json/jsMain/src/kotlinx/serialization/json/internal/FormatLanguageJs.kt
rename to formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/FormatLanguageJsWasm.kt
diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/JsonToStringWriterJs.kt b/formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/JsonToStringWriterJsWasm.kt
similarity index 95%
rename from formats/json/jsMain/src/kotlinx/serialization/json/internal/JsonToStringWriterJs.kt
rename to formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/JsonToStringWriterJsWasm.kt
index 806a1d8..387ed7e 100644
--- a/formats/json/jsMain/src/kotlinx/serialization/json/internal/JsonToStringWriterJs.kt
+++ b/formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/JsonToStringWriterJsWasm.kt
@@ -1,6 +1,6 @@
 package kotlinx.serialization.json.internal
 
-internal actual open class JsonToStringWriter actual constructor(): JsonWriter {
+internal actual open class JsonToStringWriter actual constructor(): InternalJsonWriter {
     private val sb = StringBuilder(128)
 
     actual override fun writeLong(value: Long) {
diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/createMapForCache.kt b/formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/createMapForCache.kt
similarity index 100%
rename from formats/json/jsMain/src/kotlinx/serialization/json/internal/createMapForCache.kt
rename to formats/json/jsWasmMain/src/kotlinx/serialization/json/internal/createMapForCache.kt
diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt
index 4af1045..329e309 100644
--- a/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt
+++ b/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt
@@ -22,7 +22,7 @@
 ) {
     val writer = JsonToJavaStreamWriter(stream)
     try {
-        encodeByWriter(writer, serializer, value)
+        encodeByWriter(this, writer, serializer, value)
     } finally {
         writer.release()
     }
@@ -58,7 +58,7 @@
 ): T {
     val reader = JavaStreamSerialReader(stream)
     try {
-        return decodeByReader(deserializer, reader)
+        return decodeByReader(this, deserializer, reader)
     } finally {
         reader.release()
     }
@@ -102,7 +102,7 @@
     deserializer: DeserializationStrategy<T>,
     format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
 ): Sequence<T> {
-    return decodeToSequenceByReader(JavaStreamSerialReader(stream), deserializer, format)
+    return decodeToSequenceByReader(this, JavaStreamSerialReader(stream), deserializer, format)
 }
 
 /**
diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
index e11c21c..5666724 100644
--- a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
+++ b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
@@ -25,7 +25,7 @@
  * 3) We pool char arrays in order to save excess resizes, allocations
  *    and nulls-out of arrays.
  */
-internal actual class JsonToStringWriter : JsonWriter {
+internal actual class JsonToStringWriter : InternalJsonWriter {
     private var array: CharArray = CharArrayPool.take()
     private var size = 0
 
diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt
index 274dd4d..e8e0d9a 100644
--- a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt
+++ b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt
@@ -3,7 +3,7 @@
 import java.io.InputStream
 import java.io.OutputStream
 
-internal class JsonToJavaStreamWriter(private val stream: OutputStream) : JsonWriter {
+internal class JsonToJavaStreamWriter(private val stream: OutputStream) : InternalJsonWriter {
     private val buffer = ByteArrayPool.take()
     private var charArray = CharArrayPool.take()
     private var indexInBuffer: Int = 0
@@ -253,7 +253,7 @@
     }
 }
 
-internal class JavaStreamSerialReader(stream: InputStream) : SerialReader {
+internal class JavaStreamSerialReader(stream: InputStream) : InternalJsonReader {
     // NB: not closed on purpose, it is the responsibility of the caller
     private val reader = CharsetReader(stream, Charsets.UTF_8)
 
diff --git a/formats/json/nativeMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt b/formats/json/nativeMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
index d996fc3..137f3bc 100644
--- a/formats/json/nativeMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
+++ b/formats/json/nativeMain/src/kotlinx/serialization/json/internal/JsonToStringWriter.kt
@@ -1,6 +1,6 @@
 package kotlinx.serialization.json.internal
 
-internal actual open class JsonToStringWriter actual constructor(): JsonWriter {
+internal actual open class JsonToStringWriter actual constructor(): InternalJsonWriter {
     private val sb = StringBuilder(128)
 
     actual override fun writeLong(value: Long) {
diff --git a/formats/protobuf/build.gradle b/formats/protobuf/build.gradle
index c2183b2..9f93b18 100644
--- a/formats/protobuf/build.gradle
+++ b/formats/protobuf/build.gradle
@@ -21,8 +21,11 @@
 }
 
 kotlin {
-
     sourceSets {
+        configureEach {
+            languageSettings.optIn("kotlinx.serialization.internal.CoreFriendModuleApi")
+        }
+
         commonMain {
             dependencies {
                 api project(":kotlinx-serialization-core")
diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
index d2a28b5..861e2bf 100644
--- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
+++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufDecoding.kt
@@ -3,7 +3,7 @@
  */
 
 @file:OptIn(ExperimentalSerializationApi::class)
-@file:Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+@file:Suppress("UNCHECKED_CAST")
 
 package kotlinx.serialization.protobuf.internal
 
@@ -213,7 +213,7 @@
         val mapEntrySerial =
             kotlinx.serialization.builtins.MapEntrySerializer(serializer.keySerializer, serializer.valueSerializer)
         val oldSet = (previousValue as? Map<Any?, Any?>)?.entries
-        val setOfEntries = LinkedHashSetSerializer(mapEntrySerial).merge(this, oldSet)
+        val setOfEntries = (SetSerializer(mapEntrySerial) as AbstractCollectionSerializer<Map.Entry<Any?, Any?>, Set<Map.Entry<Any?, Any?>>, *>).merge(this, oldSet)
         return setOfEntries.associateBy({ it.key }, { it.value }) as T
     }
 
diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
index 2652d5c..e746679 100644
--- a/formats/protobuf/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
+++ b/formats/protobuf/commonTest/src/kotlinx/serialization/test/CurrentPlatform.common.kt
@@ -5,7 +5,7 @@
 package kotlinx.serialization.test
 
 enum class Platform {
-    JVM, JS, NATIVE
+    JVM, JS, NATIVE, WASM
 }
 
 public expect val currentPlatform: Platform
@@ -14,3 +14,4 @@
 
 public fun isJvm(): Boolean = currentPlatform == Platform.JVM
 public fun isNative(): Boolean = currentPlatform == Platform.NATIVE
+public fun isWasm(): Boolean = currentPlatform == Platform.WASM
\ No newline at end of file
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3EnumTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3EnumTest.kt
new file mode 100644
index 0000000..7b2dda2
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3EnumTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessagesProto3Enum(
+    @ProtoNumber(21) val optionalNestedEnum: KNestedEnum = KNestedEnum.FOO,
+    @ProtoNumber(22) val optionalForeignEnum: KForeignEnum = KForeignEnum.FOREIGN_FOO,
+    @ProtoNumber(23) val optionalAliasedEnum: KAliasedEnum = KAliasedEnum.ALIAS_FOO,
+) {
+    enum class KNestedEnum {
+        @ProtoNumber(0)
+        FOO,
+
+        @ProtoNumber(1)
+        BAR,
+
+        @ProtoNumber(2)
+        BAZ,
+
+        @ProtoNumber(-1)
+        NEG;
+
+        fun toProto() = TestMessagesProto3.TestAllTypesProto3.NestedEnum.valueOf(this.name)
+    }
+
+
+    enum class KAliasedEnum {
+        @ProtoNumber(0)
+        ALIAS_FOO,
+
+        @ProtoNumber(1)
+        ALIAS_BAR,
+
+        @ProtoNumber(2)
+        ALIAS_BAZ,
+
+        @ProtoNumber(2)
+        MOO,
+
+        @ProtoNumber(2)
+        moo,
+
+        @ProtoNumber(2)
+        bAz;
+
+        fun toProto() = TestMessagesProto3.TestAllTypesProto3.AliasedEnum.valueOf(this.name)
+    }
+}
+
+enum class KForeignEnum {
+    @ProtoNumber(0)
+    FOREIGN_FOO,
+
+    @ProtoNumber(1)
+    FOREIGN_BAR,
+
+    @ProtoNumber(2)
+    FOREIGN_BAZ;
+
+    fun toProto() = TestMessagesProto3.ForeignEnum.valueOf(this.name)
+}
+
+class Proto3EnumTest {
+    @Test
+    fun default() {
+        val message = KTestMessagesProto3Enum(
+            optionalNestedEnum = KTestMessagesProto3Enum.KNestedEnum.NEG,
+            optionalForeignEnum = KForeignEnum.FOREIGN_BAR,
+            optionalAliasedEnum = KTestMessagesProto3Enum.KAliasedEnum.ALIAS_BAR
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.optionalNestedEnum.toProto(), restored.optionalNestedEnum)
+        assertEquals(message.optionalForeignEnum.toProto(), restored.optionalForeignEnum)
+        assertEquals(message.optionalAliasedEnum.toProto(), restored.optionalAliasedEnum)
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Enum>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+}
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3MapTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3MapTest.kt
new file mode 100644
index 0000000..a961424
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3MapTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import io.kotlintest.properties.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessagesProto3Map(
+    @ProtoNumber(56) val mapInt32Int32: Map<Int, Int> = emptyMap(),
+    @ProtoNumber(57) val mapInt64Int64: Map<Long, Long> = emptyMap(),
+    @ProtoNumber(58) val mapUint32Uint32: Map<UInt, UInt> = emptyMap(),
+    @ProtoNumber(59) val mapUint64Uint64: Map<ULong, ULong> = emptyMap(),
+    @ProtoNumber(60) val mapSint32Sint32: Map<Int, Int> = emptyMap(),
+    @ProtoNumber(61) val mapSint64Sint64: Map<Long, Long> = emptyMap(),
+    @ProtoNumber(62) val mapFixed32Fixed32: Map<Int, Int> = emptyMap(),
+    @ProtoNumber(63) val mapFixed64Fixed64: Map<Long, Long> = emptyMap(),
+    @ProtoNumber(64) val mapSfixed32Sfixed32: Map<Int, Int> = emptyMap(),
+    @ProtoNumber(65) val mapSfixed64Sfixed64: Map<Long, Long> = emptyMap(),
+    @ProtoNumber(66) val mapInt32Float: Map<Int, Float> = emptyMap(),
+    @ProtoNumber(67) val mapInt32Double: Map<Int, Double> = emptyMap(),
+    @ProtoNumber(68) val mapBoolBool: Map<Boolean, Boolean> = emptyMap(),
+    @ProtoNumber(69) val mapStringString: Map<String, String> = emptyMap(),
+    @ProtoNumber(70) val mapStringBytes: Map<String, ByteArray> = emptyMap(),
+    @ProtoNumber(71) val mapStringNestedMessage: Map<String, KTestMessagesProto3Message.KNestedMessage> = emptyMap(),
+    @ProtoNumber(72) val mapStringForeignMessage: Map<String, KForeignMessage> = emptyMap(),
+    @ProtoNumber(73) val mapStringNestedEnum: Map<String, KTestMessagesProto3Enum.KNestedEnum> = emptyMap(),
+    @ProtoNumber(74) val mapStringForeignEnum: Map<String, KForeignEnum> = emptyMap(),
+)
+
+class Proto3MapTest {
+    @Test
+    fun default() {
+        val message = KTestMessagesProto3Map(
+            mapInt32Int32 = Gen.map(Gen.int(), Gen.int()).generate(),
+            mapInt64Int64 = Gen.map(Gen.long(), Gen.long()).generate(),
+            mapUint32Uint32 = Gen.map(Gen.int().map { it.toUInt() }, Gen.int().map { it.toUInt() }).generate(),
+            mapUint64Uint64 = Gen.map(Gen.int().map { it.toULong() }, Gen.int().map { it.toULong() }).generate(),
+            mapInt32Float = Gen.map(Gen.int(), Gen.float()).generate(),
+            mapInt32Double = Gen.map(Gen.int(), Gen.double()).generate(),
+            mapBoolBool = Gen.map(Gen.bool(), Gen.bool()).generate(),
+            mapStringString = Gen.map(Gen.string(), Gen.string()).generate(),
+            mapStringBytes = Gen.map(Gen.string(), Gen.string().map { it.toByteArray() }).generate(),
+            mapStringNestedMessage = mapOf(
+                "asd_1" to KTestMessagesProto3Message.KNestedMessage(
+                    1,
+                    null
+                ),
+                "asi_#" to KTestMessagesProto3Message.KNestedMessage(
+                    2,
+                    KTestMessagesProto3Message(
+                        KTestMessagesProto3Message.KNestedMessage(3, null),
+                    )
+                )
+            ),
+            mapStringForeignMessage = mapOf(
+                "" to KForeignMessage(1),
+                "-2" to KForeignMessage(-12),
+            ),
+            mapStringNestedEnum = Gen.map(
+                Gen.string(), Gen.oneOf(
+                    KTestMessagesProto3Enum.KNestedEnum.entries,
+                )
+            ).generate(),
+            mapStringForeignEnum = Gen.map(
+                Gen.string(), Gen.oneOf(
+                    KForeignEnum.entries,
+                )
+            ).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+
+        assertEquals(message.mapInt32Int32, restored.mapInt32Int32Map)
+        assertEquals(message.mapInt64Int64, restored.mapInt64Int64Map)
+        assertEquals(
+            message.mapUint32Uint32,
+            restored.mapUint32Uint32Map.map { it.key.toUInt() to it.value.toUInt() }.toMap()
+        )
+        assertEquals(
+            message.mapUint64Uint64,
+            restored.mapUint64Uint64Map.map { it.key.toULong() to it.value.toULong() }.toMap()
+        )
+        assertEquals(message.mapInt32Float, restored.mapInt32FloatMap)
+        assertEquals(message.mapInt32Double, restored.mapInt32DoubleMap)
+        assertEquals(message.mapBoolBool, restored.mapBoolBoolMap)
+        assertEquals(message.mapStringString, restored.mapStringStringMap)
+        assertContentEquals(
+            message.mapStringBytes.mapValues { it.value.toString(Charsets.UTF_32) }.entries.toList(),
+            restored.mapStringBytesMap.mapValues { it.value.toByteArray().toString(Charsets.UTF_32) }.entries.toList()
+        )
+        assertEquals(
+            message.mapStringNestedMessage.mapValues { it.value.toProto() },
+            restored.mapStringNestedMessageMap
+        )
+        assertEquals(
+            message.mapStringForeignMessage.mapValues { it.value.toProto() },
+            restored.mapStringForeignMessageMap
+        )
+        assertEquals(
+            message.mapStringNestedEnum.mapValues { it.value.name },
+            restored.mapStringNestedEnumMap.mapValues { it.value.name },
+        )
+        assertEquals(
+            message.mapStringForeignEnum.mapValues { it.value.name },
+            restored.mapStringForeignEnumMap.mapValues { it.value.name }
+        )
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Map>(restored.toByteArray())
+        assertEquals(message.copy(mapStringBytes = mapOf()), restoredMessage.copy(mapStringBytes = mapOf()))
+    }
+
+    @Test
+    @Ignore
+    // Issue: https://github.com/Kotlin/kotlinx.serialization/issues/2417
+    fun signedAndFixed() {
+        val message = KTestMessagesProto3Map(
+            mapSint32Sint32 = Gen.map(Gen.int(), Gen.int()).generate(),
+            mapSint64Sint64 = Gen.map(Gen.long(), Gen.long()).generate(),
+            mapFixed32Fixed32 = Gen.map(Gen.int(), Gen.int()).generate(),
+            mapFixed64Fixed64 = Gen.map(Gen.long(), Gen.long()).generate(),
+            mapSfixed32Sfixed32 = Gen.map(Gen.int(), Gen.int()).generate(),
+            mapSfixed64Sfixed64 = Gen.map(Gen.long(), Gen.long()).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+
+        assertContentEquals(message.mapSint32Sint32.entries.toList(), restored.mapSint32Sint32Map.entries.toList())
+        assertContentEquals(message.mapSint64Sint64.entries.toList(), restored.mapSint64Sint64Map.entries.toList())
+        assertContentEquals(message.mapFixed32Fixed32.entries.toList(), restored.mapFixed32Fixed32Map.entries.toList())
+        assertContentEquals(message.mapFixed64Fixed64.entries.toList(), restored.mapFixed64Fixed64Map.entries.toList())
+        assertContentEquals(
+            message.mapSfixed32Sfixed32.entries.toList(),
+            restored.mapSfixed32Sfixed32Map.entries.toList()
+        )
+        assertContentEquals(
+            message.mapSfixed64Sfixed64.entries.toList(),
+            restored.mapSfixed64Sfixed64Map.entries.toList()
+        )
+
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Map>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+}
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3MessageTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3MessageTest.kt
new file mode 100644
index 0000000..c369d6e
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3MessageTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessagesProto3Message(
+    @ProtoNumber(18) val optionalNestedMessage: KNestedMessage? = null,
+    @ProtoNumber(19) val optionalForeignMessage: KForeignMessage? = null,
+) {
+    @Serializable
+    data class KNestedMessage(
+        @ProtoNumber(1) val a: Int = 0,
+        @ProtoNumber(2) val corecursive: KTestMessagesProto3Message? = null,
+    ) {
+        fun toProto(): TestMessagesProto3.TestAllTypesProto3.NestedMessage =
+            TestMessagesProto3.TestAllTypesProto3.NestedMessage.parseFrom(
+                ProtoBuf.encodeToByteArray(this)
+            )
+    }
+}
+
+@Serializable
+data class KForeignMessage(
+    @ProtoNumber(1) val c: Int = 0,
+) {
+    fun toProto(): TestMessagesProto3.ForeignMessage =
+        TestMessagesProto3.ForeignMessage.parseFrom(
+            ProtoBuf.encodeToByteArray(this)
+        )
+}
+
+class Proto3MessageTest {
+    @Test
+    fun default() {
+        val message = KTestMessagesProto3Message(
+            optionalNestedMessage = KTestMessagesProto3Message.KNestedMessage(
+                a = 150,
+                corecursive = KTestMessagesProto3Message(
+                    optionalNestedMessage = KTestMessagesProto3Message.KNestedMessage(
+                        a = 42,
+                    )
+                )
+            ),
+            optionalForeignMessage = KForeignMessage(
+                c = 150,
+            )
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+        assertEquals(message.optionalNestedMessage?.a, restored.optionalNestedMessage.a)
+        assertEquals(
+            message.optionalNestedMessage?.corecursive?.optionalNestedMessage?.a,
+            restored.optionalNestedMessage.corecursive.optionalNestedMessage.a
+        )
+        assertEquals(message.optionalForeignMessage?.c, restored.optionalForeignMessage.c)
+    }
+}
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3OneofTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3OneofTest.kt
new file mode 100644
index 0000000..fda811e
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3OneofTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessageProto3Oneof(
+    @ProtoNumber(111) val oneofUint32: UInt? = null,
+    @ProtoNumber(112) val oneofNestedMessage: KTestMessagesProto3Message.KNestedMessage? = null,
+    @ProtoNumber(113) val oneofString: String? = null,
+    @ProtoNumber(114) val oneofBytes: ByteArray? = null,
+    @ProtoNumber(115) val oneofBool: Boolean? = null,
+    @ProtoNumber(116) val oneofUint64: ULong? = null,
+    @ProtoNumber(117) val oneofFloat: Float? = null,
+    @ProtoNumber(118) val oneofDouble: Double? = null,
+    @ProtoNumber(119) val oneofEnum: KTestMessagesProto3Enum.KNestedEnum? = null,
+) {
+    init {
+        require(
+            listOf(
+                oneofUint32,
+                oneofNestedMessage,
+                oneofString,
+                oneofBytes,
+                oneofBool,
+                oneofUint64,
+                oneofFloat,
+                oneofDouble,
+                oneofEnum,
+            ).count { it != null } == 1
+        )
+    }
+}
+
+class Proto3OneofTest {
+
+    /**
+     * Verify that the given [KTestMessageProto3Oneof] is correctly encoded and decoded as
+     * [TestMessagesProto3.TestAllTypesProto3] by running the [verificationFunction]. This
+     * method also verifies that the encoded and decoded message is equal to the original message.
+     *
+     * @param verificationFunction a function that verifies the encoded and decoded message. First parameter
+     * is the original message and the second parameter is the decoded protobuf library message.
+     * @receiver the [KTestMessageProto3Oneof] to verify
+     */
+    private fun KTestMessageProto3Oneof.verify(
+        verificationFunction: (KTestMessageProto3Oneof, TestMessagesProto3.TestAllTypesProto3) -> Unit,
+    ) {
+        val bytes = ProtoBuf.encodeToByteArray(this)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        verificationFunction.invoke(this, restored)
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessageProto3Oneof>(restored.toByteArray())
+
+        // [equals] method is not implemented for [ByteArray] so we need to compare it separately.
+        assertEquals(this, restoredMessage.copy(oneofBytes = this.oneofBytes))
+        assertContentEquals(this.oneofBytes, restoredMessage.oneofBytes)
+    }
+
+    @Test
+    fun uint32() {
+        KTestMessageProto3Oneof(oneofUint32 = 150u).verify { self, restored ->
+            assertEquals(self.oneofUint32, restored.oneofUint32.toUInt())
+        }
+    }
+
+    @Test
+    fun nestedMessage() {
+        KTestMessageProto3Oneof(
+            oneofNestedMessage = KTestMessagesProto3Message.KNestedMessage(a = 150),
+        ).verify { self, restored ->
+            assertEquals(self.oneofNestedMessage?.a, restored.oneofNestedMessage.a)
+        }
+    }
+
+    @Test
+    fun string() {
+        KTestMessageProto3Oneof(oneofString = "150").verify { self, restored ->
+            assertEquals(self.oneofString, restored.oneofString)
+        }
+    }
+
+    @Test
+    fun bytes() {
+        KTestMessageProto3Oneof(oneofBytes = "150".toByteArray()).verify { self, restored ->
+            assertContentEquals(self.oneofBytes, restored.oneofBytes.toByteArray())
+        }
+    }
+
+    @Test
+    fun bool() {
+        KTestMessageProto3Oneof(oneofBool = true).verify { self, restored ->
+            assertEquals(self.oneofBool, restored.oneofBool)
+        }
+    }
+
+    @Test
+    fun uint64() {
+        KTestMessageProto3Oneof(oneofUint64 = 150uL).verify { self, restored ->
+            assertEquals(self.oneofUint64, restored.oneofUint64.toULong())
+        }
+    }
+
+    @Test
+    fun float() {
+        KTestMessageProto3Oneof(oneofFloat = 150f).verify { self, restored ->
+            assertEquals(self.oneofFloat, restored.oneofFloat)
+        }
+    }
+
+    @Test
+    fun double() {
+        KTestMessageProto3Oneof(oneofDouble = 150.0).verify { self, restored ->
+            assertEquals(self.oneofDouble, restored.oneofDouble)
+        }
+    }
+
+    @Test
+    fun enum() {
+        KTestMessageProto3Oneof(oneofEnum = KTestMessagesProto3Enum.KNestedEnum.BAR).verify { self, restored ->
+            assertEquals(self.oneofEnum?.name, restored.oneofEnum.name)
+        }
+    }
+}
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3PackedTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3PackedTest.kt
new file mode 100644
index 0000000..e0da0bb
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3PackedTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import io.kotlintest.properties.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessagesProto3Packed(
+    @ProtoNumber(75) @ProtoPacked val packedInt32: List<Int> = emptyList(),
+    @ProtoNumber(76) @ProtoPacked val packedInt64: List<Long> = emptyList(),
+    @ProtoNumber(77) @ProtoPacked val packedUint32: List<UInt> = emptyList(),
+    @ProtoNumber(78) @ProtoPacked val packedUint64: List<ULong> = emptyList(),
+    @ProtoNumber(79) @ProtoPacked val packedSint32: List<Int> = emptyList(),
+    @ProtoNumber(80) @ProtoPacked val packedSint64: List<Long> = emptyList(),
+    @ProtoNumber(81) @ProtoPacked val packedFixed32: List<Int> = emptyList(),
+    @ProtoNumber(82) @ProtoPacked val packedFixed64: List<Long> = emptyList(),
+    @ProtoNumber(83) @ProtoPacked val packedSfixed32: List<Int> = emptyList(),
+    @ProtoNumber(84) @ProtoPacked val packedSfixed64: List<Long> = emptyList(),
+    @ProtoNumber(85) @ProtoPacked val packedFloat: List<Float> = emptyList(),
+    @ProtoNumber(86) @ProtoPacked val packedDouble: List<Double> = emptyList(),
+    @ProtoNumber(87) @ProtoPacked val packedBool: List<Boolean> = emptyList(),
+)
+
+class Proto3PackedTest {
+    @Test
+    fun default() {
+        val message = KTestMessagesProto3Packed(
+            packedInt32 = Gen.list(Gen.int()).generate(),
+            packedInt64 = Gen.list(Gen.long()).generate(),
+            packedFloat = Gen.list(Gen.float()).generate(),
+            packedDouble = Gen.list(Gen.double()).generate(),
+            packedBool = Gen.list(Gen.bool()).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.packedInt32, restored.packedInt32List)
+        assertEquals(message.packedInt64, restored.packedInt64List)
+        assertEquals(message.packedFloat, restored.packedFloatList)
+        assertEquals(message.packedDouble, restored.packedDoubleList)
+        assertEquals(message.packedBool, restored.packedBoolList)
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Packed>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+
+    @Test
+    @Ignore
+    // Issue: https://github.com/Kotlin/kotlinx.serialization/issues/2419
+    fun signedAndFixed() {
+        val message = KTestMessagesProto3Packed(
+            packedSint32 = Gen.list(Gen.int()).generate(),
+            packedSint64 = Gen.list(Gen.long()).generate(),
+            packedFixed32 = Gen.list(Gen.int()).generate(),
+            packedFixed64 = Gen.list(Gen.long()).generate(),
+            packedSfixed32 = Gen.list(Gen.int()).generate(),
+            packedSfixed64 = Gen.list(Gen.long()).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.packedSint32, restored.packedSint32List)
+        assertEquals(message.packedSint64, restored.packedSint64List)
+        assertEquals(message.packedFixed32, restored.packedFixed32List)
+        assertEquals(message.packedFixed64, restored.packedFixed64List)
+        assertEquals(message.packedSfixed32, restored.packedSfixed32List)
+        assertEquals(message.packedSfixed64, restored.packedSfixed64List)
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Packed>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+
+    @Test
+    @Ignore
+    // Issue: https://github.com/Kotlin/kotlinx.serialization/issues/2418
+    fun unsigned() {
+        val message = KTestMessagesProto3Packed(
+            packedUint32 = Gen.list(Gen.int().map { it.toUInt() }).generate(),
+            packedUint64 = Gen.list(Gen.long().map { it.toULong() }).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.packedUint32, restored.packedUint32List.map { it.toUInt() })
+        assertEquals(message.packedUint64, restored.packedUint64List.map { it.toULong() })
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Packed>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+}
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3PrimitiveTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3PrimitiveTest.kt
new file mode 100644
index 0000000..a7363f8
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3PrimitiveTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessagesProto3Primitive(
+    @ProtoNumber(1) val optionalInt32: Int = 0,
+    @ProtoNumber(2) val optionalInt64: Long = 0,
+    @ProtoNumber(3) val optionalUint32: UInt = 0U,
+    @ProtoNumber(4) val optionalUint64: ULong = 0UL,
+    @ProtoNumber(5) @ProtoType(ProtoIntegerType.SIGNED) val optionalSint32: Int = 0,
+    @ProtoNumber(6) @ProtoType(ProtoIntegerType.SIGNED) val optionalSint64: Long = 0,
+    @ProtoNumber(7) @ProtoType(ProtoIntegerType.FIXED) val optionalFixed32: Int = 0,
+    @ProtoNumber(8) @ProtoType(ProtoIntegerType.FIXED) val optionalFixed64: Long = 0,
+    @ProtoNumber(9) @ProtoType(ProtoIntegerType.FIXED) val optionalSfixed32: Int = 0,
+    @ProtoNumber(10) @ProtoType(ProtoIntegerType.FIXED) val optionalSfixed64: Long = 0,
+    @ProtoNumber(11) val optionalFloat: Float = 0.0f,
+    @ProtoNumber(12) val optionalDouble: Double = 0.0,
+    @ProtoNumber(13) val optionalBool: Boolean = false,
+    @ProtoNumber(14) val optionalString: String = "",
+    @ProtoNumber(15) val optionalBytes: ByteArray = byteArrayOf(),
+)
+
+class Proto3PrimitiveTest {
+    @Test
+    fun default() {
+        val message = KTestMessagesProto3Primitive(
+            optionalInt32 = Int.MAX_VALUE,
+            optionalInt64 = Long.MAX_VALUE,
+            optionalUint32 = UInt.MAX_VALUE,
+            optionalUint64 = ULong.MAX_VALUE,
+            optionalSint32 = Int.MAX_VALUE,
+            optionalSint64 = Long.MAX_VALUE,
+            optionalFixed32 = Int.MAX_VALUE,
+            optionalFixed64 = Long.MAX_VALUE,
+            optionalSfixed32 = Int.MAX_VALUE,
+            optionalSfixed64 = Long.MAX_VALUE,
+            optionalFloat = Float.MAX_VALUE,
+            optionalDouble = Double.MAX_VALUE,
+            optionalBool = true,
+            optionalString = "string",
+            optionalBytes = byteArrayOf(1, 2, 3, 4, 5)
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.optionalInt32, restored.optionalInt32)
+        assertEquals(message.optionalInt64, restored.optionalInt64)
+        assertEquals(message.optionalUint32, restored.optionalUint32.toUInt())
+        assertEquals(message.optionalUint64, restored.optionalUint64.toULong())
+        assertEquals(message.optionalSint32, restored.optionalSint32)
+        assertEquals(message.optionalSint64, restored.optionalSint64)
+        assertEquals(message.optionalFixed32, restored.optionalFixed32)
+        assertEquals(message.optionalFixed64, restored.optionalFixed64)
+        assertEquals(message.optionalSfixed32, restored.optionalSfixed32)
+        assertEquals(message.optionalSfixed64, restored.optionalSfixed64)
+        assertEquals(message.optionalFloat, restored.optionalFloat)
+        assertEquals(message.optionalDouble, restored.optionalDouble)
+        assertEquals(message.optionalBool, restored.optionalBool)
+        assertEquals(message.optionalString, restored.optionalString)
+        assertContentEquals(message.optionalBytes, restored.optionalBytes.toByteArray())
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Primitive>(restored.toByteArray())
+
+        // [equals] method is not implemented for [ByteArray] so we need to compare it separately.
+        assertEquals(message, restoredMessage.copy(optionalBytes = message.optionalBytes))
+        assertContentEquals(message.optionalBytes, restoredMessage.optionalBytes)
+    }
+}
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3RepeatedTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3RepeatedTest.kt
new file mode 100644
index 0000000..b3dab8c
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3RepeatedTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import io.kotlintest.properties.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessagesProto3Repeated(
+    @ProtoNumber(31) @ProtoPacked val repeatedInt32: List<Int> = emptyList(),
+    @ProtoNumber(32) @ProtoPacked val repeatedInt64: List<Long> = emptyList(),
+    @ProtoNumber(33) @ProtoPacked val repeatedUint32: List<UInt> = emptyList(),
+    @ProtoNumber(34) @ProtoPacked val repeatedUint64: List<ULong> = emptyList(),
+    @ProtoNumber(35) @ProtoPacked val repeatedSint32: List<Int> = emptyList(),
+    @ProtoNumber(36) @ProtoPacked val repeatedSint64: List<Long> = emptyList(),
+    @ProtoNumber(37) @ProtoPacked val repeatedFixed32: List<Int> = emptyList(),
+    @ProtoNumber(38) @ProtoPacked val repeatedFixed64: List<Long> = emptyList(),
+    @ProtoNumber(39) @ProtoPacked val repeatedSfixed32: List<Int> = emptyList(),
+    @ProtoNumber(40) @ProtoPacked val repeatedSfixed64: List<Long> = emptyList(),
+    @ProtoNumber(41) @ProtoPacked val repeatedFloat: List<Float> = emptyList(),
+    @ProtoNumber(42) @ProtoPacked val repeatedDouble: List<Double> = emptyList(),
+    @ProtoNumber(43) @ProtoPacked val repeatedBool: List<Boolean> = emptyList(),
+    @ProtoNumber(44) val repeatedString: List<String> = emptyList(),
+    @ProtoNumber(45) val repeatedBytes: List<ByteArray> = emptyList(),
+    @ProtoNumber(48) val repeatedNestedMessages: List<KTestMessagesProto3Message.KNestedMessage> = emptyList(),
+    @ProtoNumber(49) val repeatedForeignMessages: List<KForeignMessage> = emptyList(),
+)
+
+class Proto3RepeatedTest {
+    @Test
+    fun default() {
+        val message = KTestMessagesProto3Repeated(
+            repeatedInt32 = Gen.list(Gen.int()).generate(),
+            repeatedInt64 = Gen.list(Gen.long()).generate(),
+            repeatedFloat = Gen.list(Gen.float()).generate(),
+            repeatedDouble = Gen.list(Gen.double()).generate(),
+            repeatedBool = Gen.list(Gen.bool()).generate(),
+            repeatedString = Gen.list(Gen.string()).generate(),
+            repeatedBytes = Gen.list(Gen.string().map { it.toByteArray() }).generate(),
+            repeatedNestedMessages = listOf(
+                KTestMessagesProto3Message.KNestedMessage(
+                    1,
+                    null
+                ),
+                KTestMessagesProto3Message.KNestedMessage(
+                    2,
+                    KTestMessagesProto3Message(
+                        KTestMessagesProto3Message.KNestedMessage(3, null),
+                    )
+                )
+            ),
+            repeatedForeignMessages = listOf(
+                KForeignMessage(1),
+                KForeignMessage(-12),
+            )
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.repeatedInt32, restored.repeatedInt32List)
+        assertEquals(message.repeatedInt64, restored.repeatedInt64List)
+        assertEquals(message.repeatedFloat, restored.repeatedFloatList)
+        assertEquals(message.repeatedDouble, restored.repeatedDoubleList)
+        assertEquals(message.repeatedBool, restored.repeatedBoolList)
+        assertEquals(message.repeatedString, restored.repeatedStringList)
+        assertEquals(message.repeatedNestedMessages.map { it.toProto() }, restored.repeatedNestedMessageList)
+        assertEquals(message.repeatedForeignMessages.map { it.toProto() }, restored.repeatedForeignMessageList)
+        assertEquals(message.repeatedBytes.map { it.toList() }, restored.repeatedBytesList.map { it.toList() })
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Repeated>(restored.toByteArray())
+        // [equals] method is not implemented for [ByteArray] so we need to compare it separately.
+        assertEquals(message, restoredMessage.copy(repeatedBytes = message.repeatedBytes))
+        assertContentEquals(
+            message.repeatedBytes.flatMap { it.toList() },
+            restoredMessage.repeatedBytes.flatMap { it.toList() },
+        )
+    }
+
+    @Test
+    @Ignore
+    // Issue: https://github.com/Kotlin/kotlinx.serialization/issues/2419
+    fun signedAndFixed() {
+        val message = KTestMessagesProto3Repeated(
+            repeatedSint32 = Gen.list(Gen.int()).generate(),
+            repeatedSint64 = Gen.list(Gen.long()).generate(),
+            repeatedFixed32 = Gen.list(Gen.int()).generate(),
+            repeatedFixed64 = Gen.list(Gen.long()).generate(),
+            repeatedSfixed32 = Gen.list(Gen.int()).generate(),
+            repeatedSfixed64 = Gen.list(Gen.long()).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.repeatedUint32, restored.repeatedUint32List.map { it.toUInt() })
+        assertEquals(message.repeatedUint64, restored.repeatedUint64List.map { it.toULong() })
+        assertEquals(message.repeatedSint32, restored.repeatedSint32List)
+        assertEquals(message.repeatedSint64, restored.repeatedSint64List)
+        assertEquals(message.repeatedFixed32, restored.repeatedFixed32List)
+        assertEquals(message.repeatedFixed64, restored.repeatedFixed64List)
+        assertEquals(message.repeatedSfixed32, restored.repeatedSfixed32List)
+        assertEquals(message.repeatedSfixed64, restored.repeatedSfixed64List)
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Repeated>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+
+
+    @Test
+    @Ignore
+    // Issue: https://github.com/Kotlin/kotlinx.serialization/issues/2418
+    fun unsigned() {
+        val message = KTestMessagesProto3Repeated(
+            repeatedUint32 = Gen.list(Gen.int().map { it.toUInt() }).generate(),
+            repeatedUint64 = Gen.list(Gen.long().map { it.toULong() }).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.repeatedUint32, restored.repeatedUint32List.map { it.toUInt() })
+        assertEquals(message.repeatedUint64, restored.repeatedUint64List.map { it.toULong() })
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Repeated>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+}
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3UnpackedTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3UnpackedTest.kt
new file mode 100644
index 0000000..dad773d
--- /dev/null
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/conformance/Proto3UnpackedTest.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.protobuf.conformance
+
+import com.google.protobuf_test_messages.proto3.*
+import io.kotlintest.properties.*
+import kotlinx.serialization.*
+import kotlinx.serialization.protobuf.*
+import kotlin.test.*
+
+@Serializable
+data class KTestMessagesProto3Unpacked(
+    @ProtoNumber(89) val unpackedInt32: List<Int> = emptyList(),
+    @ProtoNumber(90) val unpackedInt64: List<Long> = emptyList(),
+    @ProtoNumber(91) val unpackedUint32: List<UInt> = emptyList(),
+    @ProtoNumber(92) val unpackedUint64: List<ULong> = emptyList(),
+    @ProtoNumber(93) val unpackedSint32: List<Int> = emptyList(),
+    @ProtoNumber(94) val unpackedSint64: List<Long> = emptyList(),
+    @ProtoNumber(95) val unpackedFixed32: List<Int> = emptyList(),
+    @ProtoNumber(96) val unpackedFixed64: List<Long> = emptyList(),
+    @ProtoNumber(97) val unpackedSfixed32: List<Int> = emptyList(),
+    @ProtoNumber(98) val unpackedSfixed64: List<Long> = emptyList(),
+    @ProtoNumber(99) val unpackedFloat: List<Float> = emptyList(),
+    @ProtoNumber(100) val unpackedDouble: List<Double> = emptyList(),
+    @ProtoNumber(101) val unpackedBool: List<Boolean> = emptyList(),
+)
+
+class Proto3UnpackedTest {
+    @Test
+    fun default() {
+        val message = KTestMessagesProto3Unpacked(
+            unpackedInt32 = Gen.list(Gen.int()).generate(),
+            unpackedInt64 = Gen.list(Gen.long()).generate(),
+            unpackedUint32 = Gen.list(Gen.int().map { it.toUInt() }).generate(),
+            unpackedUint64 = Gen.list(Gen.long().map { it.toULong() }).generate(),
+            unpackedFloat = Gen.list(Gen.float()).generate(),
+            unpackedDouble = Gen.list(Gen.double()).generate(),
+            unpackedBool = Gen.list(Gen.bool()).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.unpackedInt32, restored.unpackedInt32List)
+        assertEquals(message.unpackedInt64, restored.unpackedInt64List)
+        assertEquals(message.unpackedUint32, restored.unpackedUint32List.map { it.toUInt() })
+        assertEquals(message.unpackedUint64, restored.unpackedUint64List.map { it.toULong() })
+        assertEquals(message.unpackedFloat, restored.unpackedFloatList)
+        assertEquals(message.unpackedDouble, restored.unpackedDoubleList)
+        assertEquals(message.unpackedBool, restored.unpackedBoolList)
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Unpacked>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+
+    @Test
+    @Ignore
+    // Issue: https://github.com/Kotlin/kotlinx.serialization/issues/2419
+    fun signedAndFixed() {
+        val message = KTestMessagesProto3Unpacked(
+            unpackedSint32 = Gen.list(Gen.int()).generate(),
+            unpackedSint64 = Gen.list(Gen.long()).generate(),
+            unpackedFixed32 = Gen.list(Gen.int()).generate(),
+            unpackedFixed64 = Gen.list(Gen.long()).generate(),
+            unpackedSfixed32 = Gen.list(Gen.int()).generate(),
+            unpackedSfixed64 = Gen.list(Gen.long()).generate(),
+        )
+
+        val bytes = ProtoBuf.encodeToByteArray(message)
+        val restored = TestMessagesProto3.TestAllTypesProto3.parseFrom(bytes)
+
+        assertEquals(message.unpackedSint32, restored.unpackedSint32List)
+        assertEquals(message.unpackedSint64, restored.unpackedSint64List)
+        assertEquals(message.unpackedFixed32, restored.unpackedFixed32List)
+        assertEquals(message.unpackedFixed64, restored.unpackedFixed64List)
+        assertEquals(message.unpackedSfixed32, restored.unpackedSfixed32List)
+        assertEquals(message.unpackedSfixed64, restored.unpackedSfixed64List)
+
+        val restoredMessage = ProtoBuf.decodeFromByteArray<KTestMessagesProto3Unpacked>(restored.toByteArray())
+        assertEquals(message, restoredMessage)
+    }
+}
diff --git a/formats/protobuf/testProto/test_messages_proto3.proto b/formats/protobuf/testProto/test_messages_proto3.proto
new file mode 100644
index 0000000..6b27995
--- /dev/null
+++ b/formats/protobuf/testProto/test_messages_proto3.proto
@@ -0,0 +1,289 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// Test schema for proto3 messages.  This test schema is used by:
+//
+// - benchmarks
+// - fuzz tests
+// - conformance tests
+//
+// https://github.com/protocolbuffers/protobuf/blob/5e03386555544e39c21236dca0097123edec8769/src/google/protobuf/test_messages_proto3.proto
+
+syntax = "proto3";
+
+package protobuf_test_messages.proto3;
+
+option java_package = "com.google.protobuf_test_messages.proto3";
+option objc_class_prefix = "Proto3";
+
+// This is the default, but we specify it here explicitly.
+option optimize_for = SPEED;
+
+import "google/protobuf/any.proto";
+import "google/protobuf/duration.proto";
+import "google/protobuf/field_mask.proto";
+import "google/protobuf/struct.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+option cc_enable_arenas = true;
+
+// This proto includes every type of field in both singular and repeated
+// forms.
+//
+// Also, crucially, all messages and enums in this file are eventually
+// submessages of this message.  So for example, a fuzz test of TestAllTypes
+// could trigger bugs that occur in any message type in this file.  We verify
+// this stays true in a unit test.
+message TestAllTypesProto3 {
+  message NestedMessage {
+    int32 a = 1;
+    TestAllTypesProto3 corecursive = 2;
+  }
+
+  enum NestedEnum {
+    FOO = 0;
+    BAR = 1;
+    BAZ = 2;
+    NEG = -1;  // Intentionally negative.
+  }
+
+  enum AliasedEnum {
+    option allow_alias = true;
+
+    ALIAS_FOO = 0;
+    ALIAS_BAR = 1;
+    ALIAS_BAZ = 2;
+    MOO = 2;
+    moo = 2;
+    bAz = 2;
+  }
+
+  // Singular
+  int32 optional_int32 = 1;
+  int64 optional_int64 = 2;
+  uint32 optional_uint32 = 3;
+  uint64 optional_uint64 = 4;
+  sint32 optional_sint32 = 5;
+  sint64 optional_sint64 = 6;
+  fixed32 optional_fixed32 = 7;
+  fixed64 optional_fixed64 = 8;
+  sfixed32 optional_sfixed32 = 9;
+  sfixed64 optional_sfixed64 = 10;
+  float optional_float = 11;
+  double optional_double = 12;
+  bool optional_bool = 13;
+  string optional_string = 14;
+  bytes optional_bytes = 15;
+
+  NestedMessage optional_nested_message = 18;
+  ForeignMessage optional_foreign_message = 19;
+
+  NestedEnum optional_nested_enum = 21;
+  ForeignEnum optional_foreign_enum = 22;
+  AliasedEnum optional_aliased_enum = 23;
+
+  string optional_string_piece = 24 [ctype = STRING_PIECE];
+  string optional_cord = 25 [ctype = CORD];
+
+  TestAllTypesProto3 recursive_message = 27;
+
+  // Repeated
+  repeated int32 repeated_int32 = 31;
+  repeated int64 repeated_int64 = 32;
+  repeated uint32 repeated_uint32 = 33;
+  repeated uint64 repeated_uint64 = 34;
+  repeated sint32 repeated_sint32 = 35;
+  repeated sint64 repeated_sint64 = 36;
+  repeated fixed32 repeated_fixed32 = 37;
+  repeated fixed64 repeated_fixed64 = 38;
+  repeated sfixed32 repeated_sfixed32 = 39;
+  repeated sfixed64 repeated_sfixed64 = 40;
+  repeated float repeated_float = 41;
+  repeated double repeated_double = 42;
+  repeated bool repeated_bool = 43;
+  repeated string repeated_string = 44;
+  repeated bytes repeated_bytes = 45;
+
+  repeated NestedMessage repeated_nested_message = 48;
+  repeated ForeignMessage repeated_foreign_message = 49;
+
+  repeated NestedEnum repeated_nested_enum = 51;
+  repeated ForeignEnum repeated_foreign_enum = 52;
+
+  repeated string repeated_string_piece = 54 [ctype = STRING_PIECE];
+  repeated string repeated_cord = 55 [ctype = CORD];
+
+  // Packed
+  repeated int32 packed_int32 = 75 [packed = true];
+  repeated int64 packed_int64 = 76 [packed = true];
+  repeated uint32 packed_uint32 = 77 [packed = true];
+  repeated uint64 packed_uint64 = 78 [packed = true];
+  repeated sint32 packed_sint32 = 79 [packed = true];
+  repeated sint64 packed_sint64 = 80 [packed = true];
+  repeated fixed32 packed_fixed32 = 81 [packed = true];
+  repeated fixed64 packed_fixed64 = 82 [packed = true];
+  repeated sfixed32 packed_sfixed32 = 83 [packed = true];
+  repeated sfixed64 packed_sfixed64 = 84 [packed = true];
+  repeated float packed_float = 85 [packed = true];
+  repeated double packed_double = 86 [packed = true];
+  repeated bool packed_bool = 87 [packed = true];
+  repeated NestedEnum packed_nested_enum = 88 [packed = true];
+
+  // Unpacked
+  repeated int32 unpacked_int32 = 89 [packed = false];
+  repeated int64 unpacked_int64 = 90 [packed = false];
+  repeated uint32 unpacked_uint32 = 91 [packed = false];
+  repeated uint64 unpacked_uint64 = 92 [packed = false];
+  repeated sint32 unpacked_sint32 = 93 [packed = false];
+  repeated sint64 unpacked_sint64 = 94 [packed = false];
+  repeated fixed32 unpacked_fixed32 = 95 [packed = false];
+  repeated fixed64 unpacked_fixed64 = 96 [packed = false];
+  repeated sfixed32 unpacked_sfixed32 = 97 [packed = false];
+  repeated sfixed64 unpacked_sfixed64 = 98 [packed = false];
+  repeated float unpacked_float = 99 [packed = false];
+  repeated double unpacked_double = 100 [packed = false];
+  repeated bool unpacked_bool = 101 [packed = false];
+  repeated NestedEnum unpacked_nested_enum = 102 [packed = false];
+
+  // Map
+  map<int32, int32> map_int32_int32 = 56;
+  map<int64, int64> map_int64_int64 = 57;
+  map<uint32, uint32> map_uint32_uint32 = 58;
+  map<uint64, uint64> map_uint64_uint64 = 59;
+  map<sint32, sint32> map_sint32_sint32 = 60;
+  map<sint64, sint64> map_sint64_sint64 = 61;
+  map<fixed32, fixed32> map_fixed32_fixed32 = 62;
+  map<fixed64, fixed64> map_fixed64_fixed64 = 63;
+  map<sfixed32, sfixed32> map_sfixed32_sfixed32 = 64;
+  map<sfixed64, sfixed64> map_sfixed64_sfixed64 = 65;
+  map<int32, float> map_int32_float = 66;
+  map<int32, double> map_int32_double = 67;
+  map<bool, bool> map_bool_bool = 68;
+  map<string, string> map_string_string = 69;
+  map<string, bytes> map_string_bytes = 70;
+  map<string, NestedMessage> map_string_nested_message = 71;
+  map<string, ForeignMessage> map_string_foreign_message = 72;
+  map<string, NestedEnum> map_string_nested_enum = 73;
+  map<string, ForeignEnum> map_string_foreign_enum = 74;
+
+  oneof oneof_field {
+    uint32 oneof_uint32 = 111;
+    NestedMessage oneof_nested_message = 112;
+    string oneof_string = 113;
+    bytes oneof_bytes = 114;
+    bool oneof_bool = 115;
+    uint64 oneof_uint64 = 116;
+    float oneof_float = 117;
+    double oneof_double = 118;
+    NestedEnum oneof_enum = 119;
+    google.protobuf.NullValue oneof_null_value = 120;
+  }
+
+  // Well-known types
+  google.protobuf.BoolValue optional_bool_wrapper = 201;
+  google.protobuf.Int32Value optional_int32_wrapper = 202;
+  google.protobuf.Int64Value optional_int64_wrapper = 203;
+  google.protobuf.UInt32Value optional_uint32_wrapper = 204;
+  google.protobuf.UInt64Value optional_uint64_wrapper = 205;
+  google.protobuf.FloatValue optional_float_wrapper = 206;
+  google.protobuf.DoubleValue optional_double_wrapper = 207;
+  google.protobuf.StringValue optional_string_wrapper = 208;
+  google.protobuf.BytesValue optional_bytes_wrapper = 209;
+
+  repeated google.protobuf.BoolValue repeated_bool_wrapper = 211;
+  repeated google.protobuf.Int32Value repeated_int32_wrapper = 212;
+  repeated google.protobuf.Int64Value repeated_int64_wrapper = 213;
+  repeated google.protobuf.UInt32Value repeated_uint32_wrapper = 214;
+  repeated google.protobuf.UInt64Value repeated_uint64_wrapper = 215;
+  repeated google.protobuf.FloatValue repeated_float_wrapper = 216;
+  repeated google.protobuf.DoubleValue repeated_double_wrapper = 217;
+  repeated google.protobuf.StringValue repeated_string_wrapper = 218;
+  repeated google.protobuf.BytesValue repeated_bytes_wrapper = 219;
+
+  google.protobuf.Duration optional_duration = 301;
+  google.protobuf.Timestamp optional_timestamp = 302;
+  google.protobuf.FieldMask optional_field_mask = 303;
+  google.protobuf.Struct optional_struct = 304;
+  google.protobuf.Any optional_any = 305;
+  google.protobuf.Value optional_value = 306;
+  google.protobuf.NullValue optional_null_value = 307;
+
+  repeated google.protobuf.Duration repeated_duration = 311;
+  repeated google.protobuf.Timestamp repeated_timestamp = 312;
+  repeated google.protobuf.FieldMask repeated_fieldmask = 313;
+  repeated google.protobuf.Struct repeated_struct = 324;
+  repeated google.protobuf.Any repeated_any = 315;
+  repeated google.protobuf.Value repeated_value = 316;
+  repeated google.protobuf.ListValue repeated_list_value = 317;
+
+  // Test field-name-to-JSON-name convention.
+  // (protobuf says names can be any valid C/C++ identifier.)
+  int32 fieldname1 = 401;
+  int32 field_name2 = 402;
+  int32 _field_name3 = 403;
+  int32 field__name4_ = 404;
+  int32 field0name5 = 405;
+  int32 field_0_name6 = 406;
+  int32 fieldName7 = 407;
+  int32 FieldName8 = 408;
+  int32 field_Name9 = 409;
+  int32 Field_Name10 = 410;
+  int32 FIELD_NAME11 = 411;
+  int32 FIELD_name12 = 412;
+  int32 __field_name13 = 413;
+  int32 __Field_name14 = 414;
+  int32 field__name15 = 415;
+  int32 field__Name16 = 416;
+  int32 field_name17__ = 417;
+  int32 Field_name18__ = 418;
+
+  // Reserved for testing unknown fields
+  reserved 501 to 510;
+}
+
+message ForeignMessage {
+  int32 c = 1;
+}
+
+enum ForeignEnum {
+  FOREIGN_FOO = 0;
+  FOREIGN_BAR = 1;
+  FOREIGN_BAZ = 2;
+}
+
+message NullHypothesisProto3 {}
+
+message EnumOnlyProto3 {
+  enum Bool {
+    kFalse = 0;
+    kTrue = 1;
+  }
+}
diff --git a/formats/protobuf/wasmMain/src/kotlinx/serialization/protobuf/internal/Bytes.kt b/formats/protobuf/wasmMain/src/kotlinx/serialization/protobuf/internal/Bytes.kt
new file mode 100644
index 0000000..72fbfd0
--- /dev/null
+++ b/formats/protobuf/wasmMain/src/kotlinx/serialization/protobuf/internal/Bytes.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.protobuf.internal
+
+private fun Short.reverseBytes(): Short = (((this.toInt() and 0xff) shl 8) or ((this.toInt() and 0xffff) ushr 8)).toShort()
+
+internal actual fun Int.reverseBytes(): Int =
+    ((this and 0xffff).toShort().reverseBytes().toInt() shl 16) or ((this ushr 16).toShort().reverseBytes().toInt() and 0xffff)
+
+internal actual  fun Long.reverseBytes(): Long =
+    ((this and 0xffffffff).toInt().reverseBytes().toLong() shl 32) or ((this ushr 32).toInt()
+        .reverseBytes().toLong() and 0xffffffff)
diff --git a/formats/protobuf/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt b/formats/protobuf/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt
new file mode 100644
index 0000000..fd359b7
--- /dev/null
+++ b/formats/protobuf/wasmTest/src/kotlinx/serialization/test/CurrentPlatform.kt
@@ -0,0 +1,7 @@
+/*
+ * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.test
+
+public actual val currentPlatform: Platform = Platform.WASM
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 63aedbe..bab6b61 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,7 +5,7 @@
 group=org.jetbrains.kotlinx
 version=1.6.1-SNAPSHOT
 
-kotlin.version=1.9.0
+kotlin.version=1.9.20-RC
 
 # This version takes precedence if 'bootstrap' property passed to project
 kotlin.version.snapshot=1.9.255-SNAPSHOT
@@ -20,7 +20,7 @@
 # Only for tests
 coroutines_version=1.6.4
 kover_version=0.4.2
-okio_version=3.1.0
+okio_version=3.6.0
 
 kover.enabled=true
 
diff --git a/gradle/configure-source-sets.gradle b/gradle/configure-source-sets.gradle
index 56a2239..532e2ca 100644
--- a/gradle/configure-source-sets.gradle
+++ b/gradle/configure-source-sets.gradle
@@ -2,6 +2,8 @@
  * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
+import static KotlinVersion.*
+
 java {
     toolchain {
         languageVersion.set(JavaLanguageVersion.of(11))
@@ -40,6 +42,10 @@
         }
     }
 
+    wasmJs {
+        d8()
+    }
+
     sourceSets.all {
         kotlin.srcDirs = ["$it.name/src"]
         resources.srcDirs = ["$it.name/resources"]
@@ -90,6 +96,25 @@
             }
         }
 
+
+        wasmJsMain {
+            kotlin {
+                srcDir 'wasmMain/src'
+            }
+            dependencies {
+                api 'org.jetbrains.kotlin:kotlin-stdlib-wasm-js'
+            }
+        }
+
+        wasmJsTest {
+            kotlin {
+                srcDir 'wasmTest/src'
+            }
+            dependencies {
+                api 'org.jetbrains.kotlin:kotlin-test-wasm-js'
+            }
+        }
+
         nativeMain.dependencies {
         }
     }
@@ -112,6 +137,7 @@
                     languageVersion = rootProject.ext.kotlin_lv_override
                     freeCompilerArgs += "-Xsuppress-version-warnings"
                 }
+                freeCompilerArgs += "-Xexpect-actual-classes"
             }
         }
         compilations.main {
@@ -121,8 +147,7 @@
         }
     }
 
-    // TODO: Remove deprecated linuxArm32Hfp and mingwX86 targets in 1.9.20.
-    def targetsWithoutTestRunners = ["linuxArm32Hfp", "linuxArm64", "mingwX86"]
+    def targetsWithoutTestRunners = ["linuxArm64"]
     configure(targets) {
         // Configure additional binaries to run tests in the background
         if (["macos", "linux", "mingw"].any { name.startsWith(it) && !targetsWithoutTestRunners.contains(name) }) {
diff --git a/gradle/native-targets.gradle b/gradle/native-targets.gradle
index 880b4b6..5b7f8a4 100644
--- a/gradle/native-targets.gradle
+++ b/gradle/native-targets.gradle
@@ -2,178 +2,41 @@
  * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
  */
 
-/**
- * Specifies what subset of Native targets to build
- *
- * ALL — all possible for compilation
- * HOST — host-specific ones, without cross compilation (e.g. do not build linuxX64 on macOS host)
- * SINGLE — only for current OS (to import into IDEA / quickly run tests)
- * DISABLED — disable all Native targets (useful with Kotlin compiler built from sources)
- *
- * For HOST mode, all targets are still listed in .module file, so HOST mode is useful for release process.
- */
-enum NativeState {
-    ALL, HOST, SINGLE, DISABLED
-}
-
-def getNativeState(String description) {
-    if (description == null) return NativeState.SINGLE
-    switch (description.toLowerCase()) {
-        case 'all':
-        case 'true':
-            return NativeState.ALL
-        case 'host':
-            return NativeState.HOST
-        case 'disabled':
-            return NativeState.DISABLED
-            // 'single', 'false', etc
-        default:
-            return NativeState.SINGLE
-    }
-}
-
-project.ext.ideaActive = System.getProperty('idea.active') == 'true'
-project.ext.nativeState = getNativeState(property('native.deploy'))
-project.ext.singleTargetMode = project.ext.ideaActive || (project.ext.nativeState == NativeState.SINGLE)
-
-project.ext.nativeMainSets = []
-project.ext.nativeTestSets = []
-
-/**
- * Disables compilation but leaves the target in .module file
- */
-def disableCompilation(targets) {
-    configure(targets) {
-        compilations.all {
-            cinterops.all { project.tasks[interopProcessingTaskName].enabled = false }
-            compileKotlinTask.enabled = false
-        }
-        binaries.all { linkTask.enabled = false }
-
-        mavenPublication { publicationToDisable ->
-            tasks.withType(AbstractPublishToMaven).all {
-                onlyIf { publication != publicationToDisable }
-            }
-            tasks.withType(GenerateModuleMetadata).all {
-                onlyIf { publication.get() != publicationToDisable }
-            }
-        }
-    }
-}
-
-def getHostName() {
-    def target = System.getProperty("os.name")
-    if (target == 'Linux') return 'linux'
-    if (target.startsWith('Windows')) return 'windows'
-    if (target.startsWith('Mac')) return 'macos'
-    return 'unknown'
-}
-
 static def doesNotDependOnOkio(project) {
     return !project.name.contains("json-okio") && !project.name.contains("json-tests")
 }
 
 kotlin {
-    targets {
-        def manager = project.ext.hostManager
-        def linuxEnabled = manager.isEnabled(presets.linuxX64.konanTarget)
-        def macosEnabled = manager.isEnabled(presets.macosX64.konanTarget)
-        def winEnabled = manager.isEnabled(presets.mingwX64.konanTarget)
+    applyDefaultHierarchyTemplate {
 
-        def ideaPreset = presets.linuxX64
-        if (macosEnabled) ideaPreset = presets.macosX64
-        if (winEnabled) ideaPreset = presets.mingwX64
-        project.ext.ideaPreset = ideaPreset
-    }
+        // According to https://kotlinlang.org/docs/native-target-support.html
+        // Tier 1
+        linuxX64()
+        macosX64()
+        macosArm64()
+        iosSimulatorArm64()
+        iosX64()
 
-    targets {
-        delegate.metaClass.addTarget = { preset ->
-            def target = delegate.fromPreset(preset, preset.name)
-            project.ext.nativeMainSets.add(target.compilations['main'].kotlinSourceSets.first())
-            project.ext.nativeTestSets.add(target.compilations['test'].kotlinSourceSets.first())
-        }
-    }
+        // Tier 2
+        watchosSimulatorArm64()
+        watchosX64()
+        watchosArm32()
+        watchosArm64()
+        tvosSimulatorArm64()
+        tvosX64()
+        tvosArm64()
+        iosArm64()
+        linuxArm64()
 
-    targets {
-        if (project.ext.nativeState == NativeState.DISABLED) return
-
-        String[] versionComponents = (~/[.-]/).split(compilerVersion, 4)
-        String[] versionComponents1920 = ["1", "9", "20"]
-        def isAtLeast1920 = Arrays.compare(versionComponents, versionComponents1920) { lhs, rhs ->
-            (lhs as int) <=> (rhs as int)
-        } >= 0
-
-        if (project.ext.singleTargetMode) {
-            fromPreset(project.ext.ideaPreset, 'native')
-        } else {
-            // According to https://kotlinlang.org/docs/native-target-support.html
-            // Tier 1
-            addTarget(presets.linuxX64)
-            addTarget(presets.macosX64)
-            addTarget(presets.macosArm64)
-            addTarget(presets.iosSimulatorArm64)
-            addTarget(presets.iosX64)
-
-            // Tier 2
-            if (doesNotDependOnOkio(project)) {
-                addTarget(presets.linuxArm64)
-            }
-            addTarget(presets.watchosSimulatorArm64)
-            addTarget(presets.watchosX64)
-            addTarget(presets.watchosArm32)
-            addTarget(presets.watchosArm64)
-            addTarget(presets.tvosSimulatorArm64)
-            addTarget(presets.tvosX64)
-            addTarget(presets.tvosArm64)
-            addTarget(presets.iosArm64)
-
-            // Tier 3
-            if (doesNotDependOnOkio(project)) {
-                addTarget(presets.androidNativeArm32)
-                addTarget(presets.androidNativeArm64)
-                addTarget(presets.androidNativeX86)
-                addTarget(presets.androidNativeX64)
-                addTarget(presets.watchosDeviceArm64)
-            }
-
-            addTarget(presets.mingwX64)
-
-            // Deprecated, but were provided by kotlinx.serialization; can be removed only in 1.9.20 release.
-            if (!isAtLeast1920) {
-                addTarget(presets.watchosX86)
-
-                if (doesNotDependOnOkio(project)) {
-                    addTarget(presets.iosArm32)
-                    addTarget(presets.linuxArm32Hfp)
-                    addTarget(presets.mingwX86)
-                }
-            }
-
-        }
-
-        if (project.ext.nativeState == NativeState.HOST) {
-            // linux targets that can cross-compile on all three hosts
-            def linuxCrossCompileTargets = isAtLeast1920 ? [presets.linuxX64] : [presets.linuxX64, presets.linuxArm32Hfp, presets.linuxArm64]
-            if (getHostName() != "linux") {
-                disableCompilation(linuxCrossCompileTargets)
-            }
-        }
-    }
-
-
-    sourceSets {
-        nativeMain { dependsOn commonMain }
-        // Empty source set is required in order to have native tests task
-        nativeTest { dependsOn commonTest }
-
-        if (!project.ext.singleTargetMode) {
-            configure(project.ext.nativeMainSets) {
-                dependsOn nativeMain
-            }
-
-            configure(project.ext.nativeTestSets) {
-                dependsOn nativeTest
-            }
+        // Tier 3
+        mingwX64()
+        // https://github.com/square/okio/issues/1242#issuecomment-1759357336
+        if (doesNotDependOnOkio(project)) {
+            androidNativeArm32()
+            androidNativeArm64()
+            androidNativeX86()
+            androidNativeX64()
+            watchosDeviceArm64()
         }
     }
 }
diff --git a/gradle/teamcity.gradle b/gradle/teamcity.gradle
index ceb8a81..950494d 100644
--- a/gradle/teamcity.gradle
+++ b/gradle/teamcity.gradle
@@ -3,7 +3,7 @@
  */
 
 def teamcitySuffix = project.findProperty("teamcitySuffix")?.toString()
-if (project.hasProperty("teamcity") && !(build_snapshot_train || rootProject.properties['build_snapshot_up'])) {
+if (!teamcityInteractionDisabled && project.hasProperty("teamcity") && !(build_snapshot_train || rootProject.properties['build_snapshot_up'])) {
     // Tell teamcity about version number
     def postfix = (teamcitySuffix == null) ? "" : " ($teamcitySuffix)"
     println("##teamcity[buildNumber '${project.version}${postfix}']")
diff --git a/guide/test/PolymorphismTest.kt b/guide/test/PolymorphismTest.kt
index f614763..344ed24 100644
--- a/guide/test/PolymorphismTest.kt
+++ b/guide/test/PolymorphismTest.kt
@@ -23,9 +23,9 @@
     @Test
     fun testExamplePoly03() {
         captureOutput("ExamplePoly03") { example.examplePoly03.main() }.verifyOutputLinesStart(
-            "Exception in thread \"main\" kotlinx.serialization.SerializationException: Class 'OwnedProject' is not registered for polymorphic serialization in the scope of 'Project'.",
-            "To be registered automatically, class 'OwnedProject' has to be '@Serializable', and the base class 'Project' has to be sealed and '@Serializable'.",
-            "Alternatively, register the serializer for 'OwnedProject' explicitly in a corresponding SerializersModule."
+            "Exception in thread \"main\" kotlinx.serialization.SerializationException: Serializer for subclass 'OwnedProject' is not found in the polymorphic scope of 'Project'.",
+            "Check if class with serial name 'OwnedProject' exists and serializer is registered in a corresponding SerializersModule.",
+            "To be registered automatically, class 'OwnedProject' has to be '@Serializable', and the base class 'Project' has to be sealed and '@Serializable'."
         )
     }
 
@@ -133,7 +133,8 @@
     @Test
     fun testExamplePoly18() {
         captureOutput("ExamplePoly18") { example.examplePoly18.main() }.verifyOutputLinesStart(
-            "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for class discriminator 'unknown'"
+            "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 0: Serializer for subclass 'unknown' is not found in the polymorphic scope of 'Project' at path: $",
+            "Check if class with serial name 'unknown' exists and serializer is registered in a corresponding SerializersModule."
         )
     }
 
diff --git a/integration-test/build.gradle b/integration-test/build.gradle
index dda6834..225181b 100644
--- a/integration-test/build.gradle
+++ b/integration-test/build.gradle
@@ -41,15 +41,17 @@
             }
         }
     }
+    wasmJs {
+        d8()
+    }
     jvm {
         withJava()
     }
-    // For ARM, should be changed to iosArm32 or iosArm64
-    // For Linux, should be changed to e.g. linuxX64
-    // For MacOS, should be changed to e.g. macosX64
-    // For Windows, should be changed to e.g. mingwX64
-    macosX64("macos")
-    linuxX64("linux")
+    macosX64()
+    macosArm64()
+    linuxX64()
+    mingwX64()
+
     sourceSets {
         all {
             languageSettings {
@@ -95,16 +97,29 @@
                 implementation kotlin('test-js')
             }
         }
-        macosMain {
+        wasmJsMain {
             dependencies {
+                api 'org.jetbrains.kotlin:kotlin-stdlib-wasm-js'
             }
         }
-        macosTest {}
-        linuxMain {
-            kotlin.srcDirs = ["src/macosMain/kotlin"]
+
+        wasmJsTest {
+            dependencies {
+                api 'org.jetbrains.kotlin:kotlin-test-wasm-js'
+            }
         }
-        linuxTest {
-            kotlin.srcDirs = ["src/macosTest/kotlin"]
+    }
+
+    targets.all {
+        compilations.all {
+            kotlinOptions {
+                freeCompilerArgs += "-Xexpect-actual-classes"
+            }
+        }
+        compilations.main {
+            kotlinOptions {
+                allWarningsAsErrors = true
+            }
         }
     }
 }
diff --git a/integration-test/gradle.properties b/integration-test/gradle.properties
index e0e8855..a0c9623 100644
--- a/integration-test/gradle.properties
+++ b/integration-test/gradle.properties
@@ -2,7 +2,7 @@
 # Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 #
 
-mainKotlinVersion=1.9.0
+mainKotlinVersion=1.9.20-RC
 mainLibVersion=1.6.1-SNAPSHOT
 
 kotlin.code.style=official
diff --git a/integration-test/src/commonTest/kotlin/sample/JsonTest.kt b/integration-test/src/commonTest/kotlin/sample/JsonTest.kt
index 6b70435..88a7a0d 100644
--- a/integration-test/src/commonTest/kotlin/sample/JsonTest.kt
+++ b/integration-test/src/commonTest/kotlin/sample/JsonTest.kt
@@ -12,7 +12,7 @@
 import kotlin.reflect.*
 import kotlin.test.*
 
-public val jsonWithDefaults = Json { encodeDefaults = true }
+val jsonWithDefaults = Json { encodeDefaults = true }
 
 class JsonTest {
 
@@ -129,10 +129,9 @@
         assertEquals("""Derived2(state1='foo')""", restored2.toString())
     }
 
-    @Suppress("NAME_SHADOWING")
     private fun checkNotRegisteredMessage(exception: SerializationException) {
         val expectedText =
-            "is not registered for polymorphic serialization in the scope of"
+            "is not found in the polymorphic scope of"
         assertEquals(true, exception.message?.contains(expectedText))
     }
 
diff --git a/integration-test/src/macosMain/kotlin/sample/SampleMacos.kt b/integration-test/src/nativeMain/kotlin/sample/SampleMacos.kt
similarity index 100%
rename from integration-test/src/macosMain/kotlin/sample/SampleMacos.kt
rename to integration-test/src/nativeMain/kotlin/sample/SampleMacos.kt
diff --git a/integration-test/src/macosTest/kotlin/sample/SampleTestsNative.kt b/integration-test/src/nativeTest/kotlin/sample/SampleTestsNative.kt
similarity index 99%
rename from integration-test/src/macosTest/kotlin/sample/SampleTestsNative.kt
rename to integration-test/src/nativeTest/kotlin/sample/SampleTestsNative.kt
index 5ea6727..f86bf96 100644
--- a/integration-test/src/macosTest/kotlin/sample/SampleTestsNative.kt
+++ b/integration-test/src/nativeTest/kotlin/sample/SampleTestsNative.kt
@@ -8,4 +8,4 @@
     fun testHello() {
         assertTrue("Native" in hello())
     }
-}
\ No newline at end of file
+}
diff --git a/integration-test/src/wasmJsMain/kotlin/sample/SampleWasm.kt b/integration-test/src/wasmJsMain/kotlin/sample/SampleWasm.kt
new file mode 100644
index 0000000..6319036
--- /dev/null
+++ b/integration-test/src/wasmJsMain/kotlin/sample/SampleWasm.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2019 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample
+
+actual object Platform {
+    actual val name: String = "Wasm"
+}
diff --git a/integration-test/src/macosTest/kotlin/sample/SampleTestsNative.kt b/integration-test/src/wasmJsTest/kotlin/sample/SampleTestsWasm.kt
similarity index 62%
copy from integration-test/src/macosTest/kotlin/sample/SampleTestsNative.kt
copy to integration-test/src/wasmJsTest/kotlin/sample/SampleTestsWasm.kt
index 5ea6727..82dfb78 100644
--- a/integration-test/src/macosTest/kotlin/sample/SampleTestsNative.kt
+++ b/integration-test/src/wasmJsTest/kotlin/sample/SampleTestsWasm.kt
@@ -3,9 +3,9 @@
 import kotlin.test.Test
 import kotlin.test.assertTrue
 
-class SampleTestsNative {
+class SampleTestsWasm {
     @Test
     fun testHello() {
-        assertTrue("Native" in hello())
+        assertTrue("Wasm" in hello())
     }
-}
\ No newline at end of file
+}