diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api
index e402a68a7..fa1dfa655 100644
--- a/mirai-core-api/compatibility-validation/android/api/android.api
+++ b/mirai-core-api/compatibility-validation/android/api/android.api
@@ -4815,8 +4815,13 @@ public final class net/mamoe/mirai/message/data/MessageSource$Key : net/mamoe/mi
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
-public final class net/mamoe/mirai/message/data/MessageSource$Serializer : net/mamoe/mirai/internal/message/MessageSourceSerializerImpl {
+public final class net/mamoe/mirai/message/data/MessageSource$Serializer : kotlinx/serialization/KSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageSource$Serializer;
+	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageSource;
+	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageSource;)V
 }
 
 public final class net/mamoe/mirai/message/data/MessageSourceAmender : net/mamoe/mirai/message/data/MessageSourceBuilder {
diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api
index ee7ca4faa..2acb48925 100644
--- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api
+++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api
@@ -4815,8 +4815,13 @@ public final class net/mamoe/mirai/message/data/MessageSource$Key : net/mamoe/mi
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
-public final class net/mamoe/mirai/message/data/MessageSource$Serializer : net/mamoe/mirai/internal/message/MessageSourceSerializerImpl {
+public final class net/mamoe/mirai/message/data/MessageSource$Serializer : kotlinx/serialization/KSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageSource$Serializer;
+	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageSource;
+	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageSource;)V
 }
 
 public final class net/mamoe/mirai/message/data/MessageSourceAmender : net/mamoe/mirai/message/data/MessageSourceBuilder {
diff --git a/mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt b/mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt
new file mode 100644
index 000000000..ea802b871
--- /dev/null
+++ b/mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.message
+
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.encoding.*
+import net.mamoe.mirai.utils.cast
+import kotlin.jvm.JvmName
+import kotlin.reflect.KClass
+
+internal abstract class AbstractPolymorphicSerializer<T : Any> internal constructor() : KSerializer<T> {
+
+    /**
+     * Base class for all classes that this polymorphic serializer can serialize or deserialize.
+     */
+    abstract val baseClass: KClass<T>
+
+    final override fun serialize(encoder: Encoder, value: T) {
+        val actualSerializer = findPolymorphicSerializer(encoder, value)
+        encoder.encodeStructure(descriptor) {
+            encodeStringElement(descriptor, 0, actualSerializer.descriptor.serialName)
+            encodeSerializableElement(descriptor, 1, actualSerializer.cast(), value)
+        }
+    }
+
+    final override fun deserialize(decoder: Decoder): T = decoder.decodeStructure(descriptor) {
+        var klassName: String? = null
+        var value: Any? = null
+        if (decodeSequentially()) {
+            return decodeSequentially(this)
+        }
+
+        mainLoop@ while (true) {
+            when (val index = decodeElementIndex(descriptor)) {
+                CompositeDecoder.DECODE_DONE -> {
+                    break@mainLoop
+                }
+                0 -> {
+                    klassName = decodeStringElement(descriptor, index)
+                }
+                1 -> {
+                    klassName = requireNotNull(klassName) { "Cannot read polymorphic value before its type token" }
+                    val serializer = findPolymorphicSerializer(this, klassName)
+                    value = decodeSerializableElement(descriptor, index, serializer)
+                }
+                else -> throw SerializationException(
+                    "Invalid index in polymorphic deserialization of " +
+                            (klassName ?: "unknown class") +
+                            "\n Expected 0, 1 or DECODE_DONE(-1), but found $index"
+                )
+            }
+        }
+        @Suppress("UNCHECKED_CAST")
+        requireNotNull(value) { "Polymorphic value has not been read for class $klassName" } as T
+    }
+
+    private fun decodeSequentially(compositeDecoder: CompositeDecoder): T {
+        val klassName = compositeDecoder.decodeStringElement(descriptor, 0)
+        val serializer = findPolymorphicSerializer(compositeDecoder, klassName)
+        return compositeDecoder.decodeSerializableElement(descriptor, 1, serializer)
+    }
+
+    /**
+     * Lookups an actual serializer for given [klassName] withing the current [base class][baseClass].
+     * May use context from the [decoder].
+     */
+    open fun findPolymorphicSerializerOrNull(
+        decoder: CompositeDecoder,
+        klassName: String?
+    ): DeserializationStrategy<out T>? = decoder.serializersModule.getPolymorphic(baseClass, klassName)
+
+
+    /**
+     * Lookups an actual serializer for given [value] within the current [base class][baseClass].
+     * May use context from the [encoder].
+     */
+    public open fun findPolymorphicSerializerOrNull(
+        encoder: Encoder,
+        value: T
+    ): SerializationStrategy<T>? =
+        encoder.serializersModule.getPolymorphic(baseClass, value)
+}
+
+
+internal fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer(
+    decoder: CompositeDecoder,
+    klassName: String?
+): DeserializationStrategy<out T> =
+    findPolymorphicSerializerOrNull(decoder, klassName) ?: throwSubtypeNotRegistered(klassName, baseClass)
+
+internal fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer(
+    encoder: Encoder,
+    value: T
+): SerializationStrategy<T> =
+    findPolymorphicSerializerOrNull(encoder, value) ?: throwSubtypeNotRegistered(value::class, baseClass)
+
+@JvmName("throwSubtypeNotRegistered")
+internal fun throwSubtypeNotRegistered(subClassName: String?, baseClass: KClass<*>): Nothing {
+    val scope = "in the scope of '${baseClass.simpleName}'"
+    throw SerializationException(
+        if (subClassName == null)
+            "Class discriminator was missing and no default polymorphic serializers were registered $scope"
+        else
+            "Class '$subClassName' is not registered for polymorphic serialization $scope.\n" +
+                    "Mark the base class as 'sealed' or register the serializer explicitly."
+    )
+}
+
+@JvmName("throwSubtypeNotRegistered")
+internal fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing =
+    throwSubtypeNotRegistered(subClass.simpleName ?: "$subClass", baseClass)
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt
index 52061d3da..d2b15423f 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt
@@ -23,7 +23,11 @@ import kotlinx.serialization.KSerializer
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
 import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.decodeStructure
 import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.IMirai
@@ -111,6 +115,7 @@ import kotlin.native.CName
  * @see FlashImage 闪照
  * @see Image.flash 转换普通图片为闪照
  */
+@Suppress("DEPRECATION", "DEPRECATION_ERROR")
 @Serializable(Image.Serializer::class)
 @NotStableForInheritance
 public interface Image : Message, MessageContent, CodableMessage {
@@ -183,17 +188,43 @@ public interface Image : Message, MessageContent, CodableMessage {
         deserialize = { Image(it) },
     )
 
-    public object Serializer : KSerializer<Image> by FallbackSerializer("Image")
+    @Deprecated(
+        message = "For internal use only. Deprecated for removal. Please retrieve serializer from MessageSerializers.serializersModule.",
+        level = DeprecationLevel.WARNING
+    )
+    @DeprecatedSinceMirai(warningSince = "2.13") // error since 2.15, hidden since 2.16
+    public object Serializer : KSerializer<Image> by FallbackSerializer(SERIAL_NAME)
 
+    // move to mirai-core in 2.16. Delegate Serializer to the implementation from MessageSerializers.
     @MiraiInternalApi
-    public open class FallbackSerializer(serialName: String) : KSerializer<Image> by Delegate.serializer().map(
-        buildClassSerialDescriptor(serialName) { element("imageId", String.serializer().descriptor) },
-        serialize = { Delegate(imageId) },
-        deserialize = { Image(imageId) },
-    ) {
+    public open class FallbackSerializer(serialName: String) : KSerializer<Image> {
+        override val descriptor: SerialDescriptor = buildClassSerialDescriptor(serialName) {
+            element("imageId", String.serializer().descriptor)
+        }
+
+        // Note: Manually written to overcome discriminator issues.
+        // Without this implementation you will need `ignoreUnknownKeys` on deserialization.
+        override fun deserialize(decoder: Decoder): Image {
+            decoder.decodeStructure(descriptor) {
+                if (this.decodeSequentially()) {
+                    val imageId = this.decodeStringElement(descriptor, 0)
+                    return Image(imageId)
+                } else {
+                    val index = this.decodeElementIndex(descriptor)
+                    check(index == 0)
+                    val imageId = this.decodeStringElement(descriptor, index)
+                    return Image(imageId)
+                }
+            }
+        }
+
+        override fun serialize(encoder: Encoder, value: Image) {
+            Delegate.serializer().serialize(encoder, Delegate(value.imageId))
+        }
+
         @SerialName(SERIAL_NAME)
         @Serializable
-        internal data class Delegate(
+        private data class Delegate(
             val imageId: String
         )
     }
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt
index 43c2a79f0..b4796ed6b 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt
@@ -46,6 +46,8 @@ public interface MarketFace : HummerMessage {
 
     public companion object Key :
         AbstractPolymorphicMessageKey<HummerMessage, MarketFace>(HummerMessage, { it.safeCast() }) {
+        // Notice that for MarketFaceImpl, its serial name is 'MarketFace';
+        // while for Dice, that is 'Dice' instead of 'MarketFace' again. (Dice extends MarketFace)
         public const val SERIAL_NAME: String = "MarketFace"
     }
 }
\ No newline at end of file
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
index 6fd70f969..ad1199469 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
@@ -16,7 +16,9 @@ package net.mamoe.mirai.message.data
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.async
 import kotlinx.coroutines.delay
+import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonConfiguration
 import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.IMirai
@@ -25,10 +27,12 @@ import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.event.events.MessageEvent
 import net.mamoe.mirai.internal.message.MessageSourceSerializerImpl
 import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.MessageSerializers
 import net.mamoe.mirai.message.action.AsyncRecallResult
 import net.mamoe.mirai.message.data.MessageSource.Key.quote
 import net.mamoe.mirai.message.data.MessageSource.Key.recall
 import net.mamoe.mirai.message.data.visitor.MessageVisitor
+import net.mamoe.mirai.utils.DeprecatedSinceMirai
 import net.mamoe.mirai.utils.MiraiInternalApi
 import net.mamoe.mirai.utils.NotStableForInheritance
 import net.mamoe.mirai.utils.safeCast
@@ -111,6 +115,7 @@ import kotlin.jvm.JvmSynthetic
  *
  * @see buildMessageSource 构建一个 [OfflineMessageSource]
  */
+@Suppress("DEPRECATION")
 @Serializable(MessageSource.Serializer::class)
 public sealed class MessageSource : Message, MessageMetadata, ConstrainSingle {
     public final override val key: MessageKey<MessageSource>
@@ -198,9 +203,16 @@ public sealed class MessageSource : Message, MessageMetadata, ConstrainSingle {
         return visitor.visitMessageSource(this, data)
     }
 
-    public object Serializer : MessageSourceSerializerImpl("MessageSource")
+    @Deprecated("Do not use this serializer. Retrieve from `MessageSerializers.serializersModule`.")
+    @DeprecatedSinceMirai(warningSince = "2.13")
+    public object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("MessageSource")
 
     public companion object Key : AbstractMessageKey<MessageSource>({ it.safeCast() }) {
+        /**
+         * 从 [MessageSerializers] 获取到的对应[序列化器][KSerializer]在参与多态序列化时的[类型标识符][JsonConfiguration.classDiscriminator]的值.
+         *
+         * [OnlineMessageSource] 的部分属性无法通过序列化保存. 所有 [MessageSource] 子类型在序列化时都会序列化为 [OfflineMessageSource]. 反序列化时会得到 [OfflineMessageSource] 而不是原类型.
+         */
         public const val SERIAL_NAME: String = "MessageSource"
 
         /**
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt b/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt
index 82fb97681..4357587aa 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt
@@ -13,6 +13,7 @@
 
 package net.mamoe.mirai.message.data
 
+import kotlinx.serialization.Polymorphic
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import net.mamoe.mirai.message.data.MessageSource.Key.quote
@@ -47,7 +48,7 @@ public data class QuoteReply(
     /**
      * 指代被引用的消息. 其中 [MessageSource.originalMessage] 可以控制客户端显示的消息内容.
      */
-    public val source: MessageSource
+    public val source: @Polymorphic MessageSource
 ) : Message, MessageMetadata, ConstrainSingle {
     /**
      * 从消息链中获取 [MessageSource] 并构造.
diff --git a/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt b/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt
index ae7db7818..eb94988e1 100644
--- a/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt
@@ -34,4 +34,8 @@ internal data class MarketFaceImpl internal constructor(
     override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
         return visitor.ex()?.visitMarketFaceImpl(this, data) ?: super.accept(visitor, data)
     }
+
+    companion object {
+        const val SERIAL_NAME = MarketFace.SERIAL_NAME
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt
index 929a73dfe..91c4cccea 100644
--- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt
+++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt
@@ -40,11 +40,11 @@ internal class ImageProtocol : MessageProtocol() {
         add(ImagePatcherForGroup())
 
         MessageSerializer.superclassesScope(MessageContent::class, SingleMessage::class) {
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
             add(MessageSerializer(Image::class, Image.Serializer, registerAlsoContextual = true))
         }
 
         MessageSerializer.superclassesScope(Image::class, MessageContent::class, SingleMessage::class) {
-            add(MessageSerializer(Image::class, Image.Serializer))
             add(MessageSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer()))
             add(MessageSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer()))
             add(MessageSerializer(OnlineFriendImageImpl::class, OnlineFriendImageImpl.serializer()))
diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt
index 99c01e273..4e914a5c8 100644
--- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt
+++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt
@@ -20,7 +20,9 @@ import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext.Co
 import net.mamoe.mirai.internal.message.protocol.serialization.MessageSerializer
 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.utils.copy
 import net.mamoe.mirai.utils.hexToBytes
+import net.mamoe.mirai.utils.map
 
 
 internal class MarketFaceProtocol : MessageProtocol() {
@@ -30,16 +32,37 @@ internal class MarketFaceProtocol : MessageProtocol() {
 
         add(MarketFaceDecoder())
 
-        MessageSerializer.superclassesScope(MarketFace::class, MessageContent::class, SingleMessage::class) {
-            add(
-                MessageSerializer(
-                    MarketFaceImpl::class,
-                    MarketFaceImpl.serializer()
-                )
+
+        // Serialization overview:
+        // Using MarketFace as serial type:
+        // - convert data to MarketFaceImpl on serialization. Convert them back to subtypes on deserialization.
+        // - serial name is always "MarketFace"
+        // Using subtypes:
+        // - serial name is name of subtype, i.e. "MarketFace" / "Dice".
+        // - Note that we don't use MarketFaceImpl but MarketFace for compatibility concerns.
+
+        add(
+            MessageSerializer(
+                MarketFace::class, MarketFaceImpl.serializer().map(
+                    resultantDescriptor = MarketFaceImpl.serializer().descriptor.copy(MarketFace.SERIAL_NAME),
+                    deserialize = {
+                        it.delegate.toDiceOrNull() ?: it
+                    },
+                    serialize = {
+                        when (it) {
+                            is Dice -> MarketFaceImpl(it.toJceStruct())
+                            is MarketFaceImpl -> it
+                            else -> {
+                                error("Unsupported MarketFace type ${it::class.qualifiedName}")
+                            }
+                        }
+                    }
+                ), emptyArray()
             )
-        }
+        )
 
         MessageSerializer.superclassesScope(MarketFace::class, MessageContent::class, SingleMessage::class) {
+            add(MessageSerializer(MarketFaceImpl::class, MarketFaceImpl.serializer()))
             add(MessageSerializer(Dice::class, Dice.serializer()))
         }
     }
diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt
index 0198abec1..d655cd407 100644
--- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt
+++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt
@@ -36,7 +36,7 @@ internal class MusicShareProtocol : MessageProtocol() {
         add(Sender())
 
         MessageSerializer.superclassesScope(MessageContent::class, SingleMessage::class) {
-            add(MessageSerializer(MusicShare::class, MusicShare.serializer()))
+            add(MessageSerializer(MusicShare::class, MusicShare.serializer(), registerAlsoContextual = true))
         }
     }
 
diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt
index dbe99708e..8fcfd51db 100644
--- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt
+++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt
@@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.message.protocol.impl
 
 import net.mamoe.mirai.contact.AnonymousMember
 import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.internal.message.MessageSourceSerializerImpl
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
 import net.mamoe.mirai.internal.message.protocol.ProcessorCollector
 import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoder
@@ -26,6 +27,8 @@ import net.mamoe.mirai.internal.message.protocol.serialization.MessageSerializer
 import net.mamoe.mirai.internal.message.source.*
 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.utils.copy
+import net.mamoe.mirai.utils.map
 
 internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) {
     override fun ProcessorCollector.collectProcessorsImpl() {
@@ -36,6 +39,7 @@ internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) {
             currentMessageChain[QuoteReply]?.source?.ensureSequenceIdAvailable()
         })
 
+        val baseSourceSerializer = MessageSourceSerializerImpl(MessageSource.SERIAL_NAME)
         MessageSerializer.superclassesScope(MessageSource::class, MessageMetadata::class, SingleMessage::class) {
             add(
                 MessageSerializer(
@@ -92,9 +96,37 @@ internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) {
                 )
             )
 
-            add(MessageSerializer(MessageSource::class, MessageSource.serializer()))
         }
 
+        MessageSerializer.superclassesScope(MessageMetadata::class, SingleMessage::class) {
+            @Suppress("DEPRECATION")
+            add(
+                MessageSerializer(
+                    MessageSource::class,
+                    OfflineMessageSourceImplData.serializer().map(
+                        OfflineMessageSourceImplData.serializer().descriptor.copy(MessageSource.SERIAL_NAME),
+                        { it },
+                        {
+                            OfflineMessageSourceImplData(
+                                kind, ids, botId, time, fromId, targetId,
+                                originalMessage, internalIds
+                            )
+                        }
+                    ),
+                    registerAlsoContextual = true
+                )
+            )
+        }
+
+//        add(
+//            MessageSerializer(
+//                MessageSource::class,
+//                PolymorphicSerializer(MessageSource::class),
+//                emptyArray(),
+//                registerAlsoContextual = true
+//            )
+//        )
+
         MessageSerializer.superclassesScope(MessageMetadata::class, SingleMessage::class) {
             add(MessageSerializer(QuoteReply::class, QuoteReply.serializer()))
             @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt b/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt
index 02a8788d0..93b8a603e 100644
--- a/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt
+++ b/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt
@@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.message.protocol.serialization
 
 import kotlinx.serialization.KSerializer
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.message.protocol.ProcessorCollector
 import net.mamoe.mirai.message.data.SingleMessage
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
@@ -18,7 +19,8 @@ import kotlin.jvm.JvmInline
 import kotlin.reflect.KClass
 
 /**
- * Collectd in [MessageProtocol.collectProcessors]
+ * Collectd in [MessageProtocol.collectProcessors].
+ * @see ProcessorCollector.add
  */
 internal class MessageSerializer<T : Any>(
     /**
@@ -38,7 +40,7 @@ internal class MessageSerializer<T : Any>(
     // This can help native targets, which has no reflection support.
 
     companion object {
-        fun <T : SingleMessage, R> superclassesScope(
+        inline fun <T : SingleMessage, R> superclassesScope(
             vararg superclasses: KClass<in T>,
             block: SuperclassesScope<T>.() -> R
         ): R {
diff --git a/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt
index a34115f3b..fcf44b9fd 100644
--- a/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt
@@ -13,6 +13,7 @@ package net.mamoe.mirai.internal.message.source
 
 import kotlinx.atomicfu.AtomicBoolean
 import kotlinx.atomicfu.atomic
+import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Transient
 import net.mamoe.mirai.Bot
@@ -32,6 +33,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
 import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.MessageSource
 import net.mamoe.mirai.message.data.MessageSourceKind
 import net.mamoe.mirai.message.data.OnlineMessageSource
 import net.mamoe.mirai.message.data.visitor.MessageVisitor
@@ -45,7 +47,7 @@ internal class OnlineMessageSourceFromFriendImpl(
     override val bot: Bot,
     msg: List<MsgComm.Msg>,
 ) : OnlineMessageSource.Incoming.FromFriend(), IncomingMessageSourceInternal {
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromFriend")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromFriend")
 
     override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq }
     override var isRecalledOrPlanned: AtomicBoolean = atomic(false)
@@ -78,7 +80,7 @@ internal class OnlineMessageSourceFromStrangerImpl(
     override val bot: Bot,
     msg: List<MsgComm.Msg>,
 ) : OnlineMessageSource.Incoming.FromStranger(), IncomingMessageSourceInternal {
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromStranger")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromStranger")
 
     override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq }
     override var isRecalledOrPlanned: AtomicBoolean = atomic(false)
@@ -150,7 +152,7 @@ internal class OnlineMessageSourceFromTempImpl(
     override val bot: Bot,
     msg: List<MsgComm.Msg>,
 ) : OnlineMessageSource.Incoming.FromTemp(), IncomingMessageSourceInternal {
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromTemp")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromTemp")
 
     override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq }
     override val internalIds: IntArray = msg.mapToIntArray { it.msgBody.richText.attr!!.random }
@@ -187,7 +189,7 @@ internal class OnlineMessageSourceFromGroupImpl(
     override val bot: Bot,
     msg: List<MsgComm.Msg>,
 ) : OnlineMessageSource.Incoming.FromGroup(), IncomingMessageSourceInternal {
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromGroupImpl")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromGroupImpl")
 
     @Transient
     override var isRecalledOrPlanned: AtomicBoolean = atomic(false)
diff --git a/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt
index 8bd35d4b0..2e59f3876 100644
--- a/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt
@@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.message.source
 
 import kotlinx.atomicfu.AtomicBoolean
 import kotlinx.atomicfu.atomic
+import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Transient
 import net.mamoe.mirai.Bot
@@ -23,6 +24,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
 import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.MessageSource
 import net.mamoe.mirai.message.data.MessageSourceKind
 import net.mamoe.mirai.message.data.OfflineMessageSource
 import net.mamoe.mirai.message.data.visitor.MessageVisitor
@@ -42,7 +44,7 @@ internal class OfflineMessageSourceImplData(
     private val originalMessageLazy: Lazy<MessageChain>,
     override val internalIds: IntArray,
 ) : OfflineMessageSource(), MessageSourceInternal {
-    object Serializer : MessageSourceSerializerImpl("OfflineMessageSource")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OfflineMessageSource")
 
     override val sequenceIds: IntArray get() = ids
     override val originalMessage: MessageChain by originalMessageLazy
diff --git a/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt
index 59054ca8c..6e0da7976 100644
--- a/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt
@@ -14,6 +14,7 @@ package net.mamoe.mirai.internal.message.source
 import kotlinx.atomicfu.AtomicBoolean
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.*
+import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.*
@@ -88,7 +89,7 @@ internal class OnlineMessageSourceToFriendImpl(
     override val sender: Bot,
     override val target: Friend,
 ) : OnlineMessageSource.Outgoing.ToFriend(), MessageSourceInternal, OutgoingMessageSourceInternal {
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToFriend")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToFriend")
 
     override val isOriginalMessageInitialized: Boolean
         get() = true
@@ -122,7 +123,7 @@ internal class OnlineMessageSourceToStrangerImpl(
         target: Stranger,
     ) : this(delegate.ids, delegate.internalIds, delegate.time, delegate.originalMessage, delegate.sender, target)
 
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToStranger")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToStranger")
 
     override val isOriginalMessageInitialized: Boolean
         get() = true
@@ -154,7 +155,7 @@ internal class OnlineMessageSourceToTempImpl(
         target: Member,
     ) : this(delegate.ids, delegate.internalIds, delegate.time, delegate.originalMessage, delegate.sender, target)
 
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToTemp")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToTemp")
 
     override val isOriginalMessageInitialized: Boolean
         get() = true
@@ -183,7 +184,7 @@ internal class OnlineMessageSourceToGroupImpl(
     override val target: Group,
     providedSequenceIds: IntArray? = null,
 ) : OnlineMessageSource.Outgoing.ToGroup(), MessageSourceInternal, OutgoingMessageSourceInternal {
-    object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToGroup")
+    object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToGroup")
 
     override val isOriginalMessageInitialized: Boolean
         get() = true
diff --git a/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt b/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt
index d05e2c93c..8656cf41e 100644
--- a/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt
@@ -195,7 +195,7 @@ internal class MessageSerializationTest : AbstractTest() {
             serializersModule = module
             ignoreUnknownKeys = true
         }
-        val source = j.decodeFromString(MessageSource.Serializer, a)
+        val source = j.decodeFromString(MessageSerializers.serializersModule.serializer<MessageSource>(), a)
         println(source.structureToString())
         assertEquals(
             expected = Mirai.buildMessageSource(692928873, MessageSourceKind.GROUP) {
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt
index 3321a6c41..361684a35 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt
@@ -13,6 +13,14 @@ import io.ktor.utils.io.core.*
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.serialization.*
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.decodeStructure
+import kotlinx.serialization.encoding.encodeStructure
+import kotlinx.serialization.json.*
 import net.mamoe.mirai.contact.ContactOrBot
 import net.mamoe.mirai.contact.Friend
 import net.mamoe.mirai.contact.Group
@@ -41,10 +49,14 @@ import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
 import net.mamoe.mirai.internal.notice.processors.GroupExtensions
 import net.mamoe.mirai.internal.test.runBlockingUnit
+import net.mamoe.mirai.internal.testFramework.dynamicTest
+import net.mamoe.mirai.message.MessageSerializers
 import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.message.data.MessageChain.Companion.serializeToJsonString
 import net.mamoe.mirai.utils.*
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
+import kotlin.reflect.KClass
 import kotlin.test.*
 
 @OptIn(TestOnly::class)
@@ -352,4 +364,294 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler
             }
         }
     }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+    open val format: Json
+        // `serializersModule` is volatile, always return new Json instances.
+        get() = Json {
+            prettyPrint = true
+            serializersModule = MessageSerializers.serializersModule
+        }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization - polymorphism
+    ///////////////////////////////////////////////////////////////////////////
+
+    interface PolymorphicWrapper {
+        val message: SingleMessage
+    }
+
+    /**
+     * @param expectedSerialName also known as *poly discriminator*, give `null` to check for no discriminator's presence
+     */
+    protected open fun <M : SingleMessage, P : PolymorphicWrapper> testPolymorphicIn(
+        polySerializer: KSerializer<P>,
+        polyConstructor: (message: M) -> P,
+        data: M,
+        expectedSerialName: String?,
+        expectedInstance: M = data,
+    ) {
+        val string = format.encodeToString(
+            polySerializer,
+            polyConstructor(data)
+        )
+        println(string)
+        var element = format.parseToJsonElement(string)
+        element as JsonObject
+        element = element["message"] as JsonObject
+        if (expectedSerialName != null) {
+            assertEquals(expectedSerialName, element["type"]?.cast<JsonPrimitive>()?.content)
+        } else {
+            assertEquals(null, element["type"])
+        }
+        assertEquals(
+            expectedInstance,
+            format.decodeFromString(polySerializer, string).message
+        )
+    }
+
+    @Serializable
+    data class PolymorphicWrapperSingleMessage(
+        override val message: @Polymorphic SingleMessage
+    ) : PolymorphicWrapper
+
+    protected open fun <M : SingleMessage> testPolymorphicInSingleMessage(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testPolymorphicInSingleMessage") {
+        testPolymorphicIn(
+            polySerializer = PolymorphicWrapperSingleMessage.serializer(),
+            polyConstructor = ::PolymorphicWrapperSingleMessage,
+            data = data,
+            expectedSerialName = expectedSerialName,
+            expectedInstance = expectedInstance
+        )
+
+    })
+
+    @Serializable
+    data class PolymorphicWrapperMessageContent(
+        override val message: @Polymorphic MessageContent
+    ) : PolymorphicWrapper
+
+    protected open fun <M : MessageContent> testPolymorphicInMessageContent(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testPolymorphicInMessageContent") {
+        testPolymorphicIn(
+            polySerializer = PolymorphicWrapperMessageContent.serializer(),
+            polyConstructor = ::PolymorphicWrapperMessageContent,
+            data = data,
+            expectedSerialName = expectedSerialName,
+            expectedInstance = expectedInstance
+        )
+    })
+
+    @Serializable
+    data class PolymorphicWrapperMessageMetadata(
+        override val message: @Polymorphic MessageMetadata
+    ) : PolymorphicWrapper
+
+    protected open fun <M : MessageMetadata> testPolymorphicInMessageMetadata(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testPolymorphicInMessageMetadata") {
+        testPolymorphicIn(
+            polySerializer = PolymorphicWrapperMessageMetadata.serializer(),
+            polyConstructor = ::PolymorphicWrapperMessageMetadata,
+            data = data,
+            expectedSerialName = expectedSerialName,
+            expectedInstance = expectedInstance
+        )
+    })
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization - in MessageChain
+    ///////////////////////////////////////////////////////////////////////////
+
+    protected open fun <M : SingleMessage> testInsideMessageChain(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testInsideMessageChain") {
+        val chain = messageChainOf(data)
+
+        val string = chain.serializeToJsonString(format)
+        println(string)
+        val element = format.parseToJsonElement(string).jsonArray.single().jsonObject
+        assertEquals(expectedSerialName, element["type"]?.cast<JsonPrimitive>()?.content)
+
+        assertEquals(
+            expectedInstance,
+            MessageChain.deserializeFromJsonString(string).single()
+        )
+    })
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization - contextual
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Serializable
+    data class ContextualWrapper(
+        val message: @Contextual SingleMessage,
+    )
+
+    @Serializable
+    data class GenericTypedWrapper<T : SingleMessage>(
+        val message: T,
+    )
+
+    protected open fun <M : T, T : SingleMessage> testContextual(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M = data,
+        targetType: KClass<out T> = data::class,
+    ) = listOf(
+        testContextualWithWrapper<M, T>(data, expectedSerialName, expectedInstance),
+        testContextualWithStaticType(targetType, data, expectedInstance),
+        testContextualGeneric(targetType, data, expectedInstance),
+        testContextualWithoutWrapper<M, T>(data, expectedInstance)
+    ).flatten()
+
+    private fun <M : T, T : SingleMessage> testContextualWithoutWrapper(
+        data: M,
+        expectedInstance: M
+    ) = listOf(dynamicTest("testContextualWithoutWrapper") {
+        @Suppress("UNCHECKED_CAST")
+        val serializer = ContextualSerializer(data::class) as KSerializer<M>
+        val string = format.encodeToString(serializer, data)
+        println(string)
+        val element = format.parseToJsonElement(string).jsonObject
+        assertEquals(null, element["type"])
+
+        assertEquals(
+            expectedInstance,
+            format.decodeFromString(serializer, string)
+        )
+    })
+
+    private fun <M : T, T : SingleMessage> testContextualGeneric(
+        targetType: KClass<out T>,
+        data: M,
+        expectedInstance: M
+    ) = listOf(dynamicTest("testContextualGeneric") {
+        val messageSerializer: ContextualSerializer<SingleMessage> = ContextualSerializer(targetType).cast()
+
+        /**
+         * ```
+         * data class StaticTypedWrapper(
+         *     val message: T
+         * )
+         * ```
+         * without concern of generic types.
+         */
+        /**
+         * ```
+         * data class StaticTypedWrapper(
+         *     val message: T
+         * )
+         * ```
+         * without concern of generic types.
+         */
+        val serializer = GenericTypedWrapper.serializer(messageSerializer)
+
+        val string = format.encodeToString(serializer, GenericTypedWrapper(data))
+        println(string)
+        val element = format.parseToJsonElement(string).jsonObject["message"]!!.jsonObject
+
+        assertEquals(null, element["type"]?.jsonPrimitive?.content)
+
+        assertEquals(
+            expectedInstance,
+            format.decodeFromString(serializer, string).message
+        )
+    })
+
+    private fun <M : T, T : SingleMessage> testContextualWithStaticType(
+        targetType: KClass<out T>,
+        data: M,
+        expectedInstance: M,
+    ) = listOf(dynamicTest("testContextualWithStaticType") {
+        val messageSerializer: ContextualSerializer<SingleMessage> = ContextualSerializer(targetType).cast()
+
+        /**
+         * ```
+         * data class StaticTypedWrapper(
+         *     val message: T
+         * )
+         * ```
+         * without concern of generic types.
+         */
+        /**
+         * ```
+         * data class StaticTypedWrapper(
+         *     val message: T
+         * )
+         * ```
+         * without concern of generic types.
+         */
+        val serializer = object : KSerializer<SingleMessage> {
+            override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StaticTypedWrapper") {
+                element("message", messageSerializer.descriptor, listOf(Contextual()))
+            }
+
+            override fun deserialize(decoder: Decoder): SingleMessage {
+                decoder.decodeStructure(descriptor) {
+                    if (this.decodeSequentially()) {
+                        return this.decodeSerializableElement(
+                            descriptor.getElementDescriptor(0),
+                            0,
+                            messageSerializer
+                        )
+                    } else {
+                        val index = this.decodeElementIndex(descriptor)
+                        check(index == 0)
+                        return this.decodeSerializableElement(descriptor, index, messageSerializer)
+                    }
+                }
+            }
+
+            override fun serialize(encoder: Encoder, value: SingleMessage) {
+                encoder.encodeStructure(descriptor) {
+                    encodeSerializableElement(descriptor, 0, messageSerializer, value)
+                }
+            }
+        }
+
+        val string = format.encodeToString(serializer, data)
+        println(string)
+        val element = format.parseToJsonElement(string).jsonObject["message"]!!.jsonObject
+
+        assertEquals(null, element["type"]?.jsonPrimitive?.content)
+
+        assertEquals(
+            expectedInstance,
+            format.decodeFromString(serializer, string)
+        )
+    })
+
+    private fun <M : T, T : SingleMessage> testContextualWithWrapper(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M,
+        afterSerialization: (element: JsonObject) -> Unit = {}
+    ) = listOf(dynamicTest("testContextualWithWrapper") {
+        val string = format.encodeToString(ContextualWrapper.serializer(), ContextualWrapper(data))
+        println(string)
+        val element = format.parseToJsonElement(string).jsonObject["message"]!!.jsonObject
+        afterSerialization(element)
+
+        assertEquals(expectedSerialName, element["type"]?.jsonPrimitive?.content)
+
+        assertEquals(
+            expectedInstance,
+            format.decodeFromString(ContextualWrapper.serializer(), string).message
+        )
+    })
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt
index ef17a50e2..dc86c675f 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt
@@ -82,4 +82,17 @@ internal class CustomMessageProtocolTest : AbstractMessageProtocolTest() {
             message(MyCustomMessage(1))
         }.doEncoderChecks()
     }
+
+// not supported, see https://github.com/mamoe/mirai/issues/2144
+//    @TestFactory
+//    fun `test serialization`(): DynamicTestsResult {
+//        val data = MyCustomMessage(1)
+//        val serialName = "CustomMessage"
+//        return runDynamicTests(
+//            testPolymorphicInMessageMetadata(data, serialName),
+//            testPolymorphicInSingleMessage(data, serialName),
+//            testInsideMessageChain(data, serialName),
+//            testContextual(data),
+//        )
+//    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt
index 385b20bda..2980b4ec8 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt
@@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl
 
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
 import net.mamoe.mirai.internal.message.protocol.decodeAndRefineLight
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.Face
 import net.mamoe.mirai.message.data.MessageSourceKind
 import net.mamoe.mirai.message.data.messageChainOf
@@ -60,4 +63,15 @@ internal class FaceProtocolTest : AbstractMessageProtocolTest() {
 
     }
 
+    @TestFactory
+    fun `test serialization`(): DynamicTestsResult {
+        val data = Face(1)
+        val serialName = Face.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt
index 438dc8346..23f9c8f58 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt
@@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl
 
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.FileMessage
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.BeforeTest
@@ -61,4 +64,16 @@ internal class FileMessageProtocolTest : AbstractMessageProtocolTest() {
             useOrdinaryEquality()
         }.doDecoderChecks()
     }
+
+    @TestFactory
+    fun `test serialization`(): DynamicTestsResult {
+        val data = FileMessage("id", 1, "name", 2)
+        val serialName = FileMessage.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FlashImageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FlashImageProtocolTest.kt
index 20695daa0..61e2d7339 100644
Binary files a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FlashImageProtocolTest.kt and b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FlashImageProtocolTest.kt differ
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt
index 9b5875430..0f32d7ea2 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt
@@ -14,6 +14,9 @@ import net.mamoe.mirai.internal.message.LightMessageRefiner.dropMiraiInternalFla
 import net.mamoe.mirai.internal.message.data.ForwardMessageInternal
 import net.mamoe.mirai.internal.message.flags.IgnoreLengthCheck
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.cast
 import net.mamoe.mirai.utils.castUp
@@ -143,4 +146,23 @@ internal class ForwardMessageProtocolTest : AbstractMessageProtocolTest() {
 //            }
 //        }
 //    }
+
+    @TestFactory
+    fun `test serialization`(): DynamicTestsResult {
+        val data = buildForwardMessage(defaultTarget.castUp()) {
+            add(1, "senderName", time = 123, message = PlainText("simple text"))
+            add(1, "senderName", time = 123) {
+                +PlainText("simple")
+                +Face(1)
+                +Image("{90CCED1C-2D64-313B-5D66-46625CAB31D7}.jpg")
+            }
+        }
+        val serialName = ForwardMessage.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt
index 9648d45ee..d57bce8df 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt
@@ -10,13 +10,22 @@
 package net.mamoe.mirai.internal.message.protocol.impl
 
 import io.ktor.utils.io.core.*
+import kotlinx.serialization.Polymorphic
+import kotlinx.serialization.Serializable
 import net.mamoe.mirai.contact.MemberPermission
+import net.mamoe.mirai.internal.message.image.OfflineFriendImage
+import net.mamoe.mirai.internal.message.image.OfflineGroupImage
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.dynamicTest
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.ImageType
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.BeforeTest
 import kotlin.test.Test
+import kotlin.test.assertIs
 
 internal class ImageProtocolTest : AbstractMessageProtocolTest() {
     override val protocols: Array<out MessageProtocol> = arrayOf(ImageProtocol())
@@ -563,4 +572,67 @@ internal class ImageProtocolTest : AbstractMessageProtocolTest() {
     }
 
 
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Serializable
+    data class PolymorphicWrapperImage(
+        override val message: @Polymorphic Image
+    ) : PolymorphicWrapper
+
+    private fun <M : Image> testPolymorphicInImage(
+        data: M,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testPolymorphicInImage") {
+        testPolymorphicIn(
+            polySerializer = PolymorphicWrapperImage.serializer(),
+            polyConstructor = ::PolymorphicWrapperImage,
+            data = data,
+            expectedSerialName = null,
+            expectedInstance = expectedInstance,
+        )
+    })
+
+    @TestFactory
+    fun `test serialization for OfflineGroupImage`(): DynamicTestsResult {
+        val data = Image("{90CCED1C-2D64-313B-5D66-46625CAB31D7}.jpg")
+        assertIs<OfflineGroupImage>(data)
+        val serialName = Image.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInImage(data),
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+    @TestFactory
+    fun `test serialization for OfflineFriendImage type 1`(): DynamicTestsResult {
+        val data = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f") // type 1
+        assertIs<OfflineFriendImage>(data)
+        val serialName = Image.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInImage(data),
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+    @TestFactory
+    fun `test serialization for OfflineFriendImage type 2`(): DynamicTestsResult {
+        val data = Image("/000000000-3814297509-BFB7027B9354B8F899A062061D74E206") // type 1
+        assertIs<OfflineFriendImage>(data)
+        val serialName = Image.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInImage(data),
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt
index a5848fb2c..5010e119c 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt
@@ -134,4 +134,6 @@ internal class LongMessageProtocolTest : AbstractMessageProtocolTest() {
             }
         }
     }
+
+    // should add tests for refining received LongMessage to normal messages (with a MessageOrigin)
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt
index 2078cbac2..c947c114e 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt
@@ -10,10 +10,17 @@
 package net.mamoe.mirai.internal.message.protocol.impl
 
 import io.ktor.utils.io.core.*
+import kotlinx.serialization.Polymorphic
+import kotlinx.serialization.Serializable
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.internal.message.data.MarketFaceImpl
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.dynamicTest
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.Dice
+import net.mamoe.mirai.message.data.MarketFace
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.BeforeTest
 import kotlin.test.Test
@@ -194,5 +201,84 @@ internal class MarketFaceProtocolTest : AbstractMessageProtocolTest() {
         }.doBothChecks()
     }
 
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
 
+    @Serializable
+    data class PolymorphicWrapperMarketFace(
+        override val message: @Polymorphic MarketFace
+    ) : PolymorphicWrapper
+
+    @Serializable
+    data class StaticWrapperDice(
+        override val message: Dice
+    ) : PolymorphicWrapper
+
+    private fun <M : MarketFace> testPolymorphicInMarketFace(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testPolymorphicInMarketFace") {
+        testPolymorphicIn(
+            polySerializer = PolymorphicWrapperMarketFace.serializer(),
+            polyConstructor = ::PolymorphicWrapperMarketFace,
+            data = data,
+            expectedSerialName = expectedSerialName, // MarketFaceImpl is 'MarketFace', Dice is 'Dice', should include discriminator
+            expectedInstance = expectedInstance,
+        )
+    })
+
+    private fun testStaticDice(
+        data: Dice,
+        expectedInstance: Dice = data,
+    ) = listOf(dynamicTest("testStaticDice") {
+        testPolymorphicIn(
+            polySerializer = StaticWrapperDice.serializer(),
+            polyConstructor = ::StaticWrapperDice,
+            data = data,
+            expectedSerialName = null,
+            expectedInstance = expectedInstance,
+        )
+    })
+
+    @TestFactory
+    fun `test serialization for MarketFaceImpl`(): DynamicTestsResult {
+        val data = MarketFaceImpl(
+            net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                faceName = "5B E5 8F 91 E5 91 86 5D".hexToBytes(),
+                itemType = 6,
+                faceInfo = 1,
+                faceId = "71 26 44 B5 27 94 46 11 99 8A EC 31 86 75 19 D2".hexToBytes(),
+                tabId = 10278,
+                subType = 3,
+                key = "726a53a5372b7289".toByteArray(), /* 37 32 36 61 35 33 61 35 33 37 32 62 37 32 38 39 */
+                imageWidth = 200,
+                imageHeight = 200,
+                pbReserve = "0A 06 08 C8 01 10 C8 01 10 64 1A 0B 51 51 E5 A4 A7 E9 BB 84 E8 84 B8 22 40 68 74 74 70 73 3A 2F 2F 7A 62 2E 76 69 70 2E 71 71 2E 63 6F 6D 2F 69 70 3F 5F 77 76 3D 31 36 37 37 38 32 34 31 26 66 72 6F 6D 3D 61 69 6F 45 6D 6F 6A 69 4E 65 77 26 69 64 3D 31 30 38 39 31 30 2A 06 E6 9D A5 E8 87 AA 30 B5 BB B4 E3 0D 38 B5 BB B4 E3 0D 40 01 50 00".hexToBytes(),
+            )
+        )
+        val serialName = MarketFaceImpl.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMarketFace(data, serialName),
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName, targetType = MarketFace::class),
+        )
+    }
+
+    @TestFactory
+    fun `test serialization for Dice`(): DynamicTestsResult {
+        val data = Dice(1)
+        val serialName = Dice.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMarketFace(data, serialName),
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName, targetType = Dice::class),
+            testStaticDice(data),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt
index 10654b6a0..52b4c64e5 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt
@@ -16,6 +16,9 @@ import net.mamoe.mirai.internal.message.protocol.MessageProtocol
 import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy
 import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageProcessorAdapter
 import net.mamoe.mirai.internal.pipeline.replaceProcessor
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.LightApp
 import net.mamoe.mirai.message.data.MessageOrigin
 import net.mamoe.mirai.message.data.MessageOriginKind
@@ -116,4 +119,26 @@ internal class MusicShareProtocolTest : AbstractMessageProtocolTest() {
         }
     }
 
+    @TestFactory
+    fun `test serialization for MusicShare`(): DynamicTestsResult {
+        val data = MusicShare(
+            kind = NeteaseCloudMusic,
+            title = "ジェリーフィッシュ",
+            summary = "Yunomi/ローラーガール",
+            jumpUrl = "https://y.music.163.com/m/song?id=562591636&uct=QK0IOc%2FSCIO8gBNG%2Bwcbsg%3D%3D&app_version=8.7.46",
+            pictureUrl = "http://p1.music.126.net/KaYSb9oYQzhl2XBeJcj8Rg==/109951165125601702.jpg",
+            musicUrl = "http://music.163.com/song/media/outer/url?id=562591636&userid=324076307&sc=wmv&tn=",
+            brief = "[分享]ジェリーフィッシュ",
+        )
+
+        val serialName = MusicShare.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt
index 311ac19c9..91d2b7543 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt
@@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl
 
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.PokeMessage
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.BeforeTest
@@ -82,4 +85,20 @@ internal class PokeMessageProtocolTest : AbstractMessageProtocolTest() {
             message(PokeMessage.ChuoYiChuo)
         }.doEncoderChecks()
     }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+    @TestFactory
+    fun `test serialization for PokeMessage`(): DynamicTestsResult {
+        val data = PokeMessage.ChuoYiChuo
+        val serialName = PokeMessage.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt
index be127ce14..49f0ce7c5 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt
@@ -9,15 +9,19 @@
 
 package net.mamoe.mirai.internal.message.protocol.impl
 
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
 import net.mamoe.mirai.internal.message.source.OfflineMessageSourceImplData
 import net.mamoe.mirai.internal.message.toMessageChainOnline
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.dynamicTest
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.internal.utils.runCoroutineInPlace
-import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.MessageSerializers
+import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.message.data.MessageSource.Key.quote
-import net.mamoe.mirai.message.data.PlainText
-import net.mamoe.mirai.message.data.QuoteReply
-import net.mamoe.mirai.message.data.messageChainOf
 import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.Test
@@ -447,4 +451,97 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() {
     }
 
 
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+    @TestFactory
+    fun `test serialization for QuoteReply`(): DynamicTestsResult {
+        val source = MessageSourceBuilder()
+            .sender(123)
+            .target(123)
+            .messages {
+                append("test")
+            }
+            .build(123, MessageSourceKind.FRIEND)
+
+        val data = QuoteReply(source)
+
+        val serialName = QuoteReply.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageMetadata(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+
+    ///////////////////////////////////////////////////////////////////////////
+    // MessageSource serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+
+    // TODO: 2022/7/20 MessageSource 在 MessageMetadata 的 scope 多态序列化后会输出 'type' = 'MessageSource', 这是期望的行为.
+    //  但是在反序列化时会错误 unknown field 'type'
+    override val format: Json
+        get() = Json {
+            prettyPrint = true
+            serializersModule = MessageSerializers.serializersModule
+            ignoreUnknownKeys = true
+        }
+
+
+    @Serializable
+    data class PolymorphicWrapperMessageSource(
+        override val message: MessageSource
+    ) : PolymorphicWrapper
+
+    private fun <M : MessageSource> testPolymorphicInMessageSource(
+        data: M,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testPolymorphicInMessageSource") {
+        testPolymorphicIn(
+            polySerializer = PolymorphicWrapperMessageSource.serializer(),
+            polyConstructor = ::PolymorphicWrapperMessageSource,
+            data = data,
+            expectedInstance = expectedInstance,
+            expectedSerialName = null,
+        )
+    })
+
+    @TestFactory
+    fun `test serialization for OfflineMessageSource`(): DynamicTestsResult {
+        val data = MessageSourceBuilder()
+            .sender(123)
+            .target(123)
+            .messages {
+                append("test")
+            }
+            .build(123, MessageSourceKind.FRIEND)
+
+        val serialName = MessageSource.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageSource(data),
+            testPolymorphicInMessageMetadata(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+    @TestFactory
+    fun `test serialization for OnlineMessageSource`(): DynamicTestsResult {
+        val data = onlineIncomingGroupMessage[MessageSource]!!
+        val expected = (data as OnlineMessageSource).toOffline()
+
+        val serialName = MessageSource.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageSource(data, expectedInstance = expected),
+            testPolymorphicInMessageMetadata(data, serialName, expectedInstance = expected),
+            testPolymorphicInSingleMessage(data, serialName, expectedInstance = expected),
+            testInsideMessageChain(data, serialName, expectedInstance = expected),
+            testContextual(data, serialName, expectedInstance = expected),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt
index e4305f6d6..360f4c03a 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt
@@ -9,8 +9,15 @@
 
 package net.mamoe.mirai.internal.message.protocol.impl
 
+import kotlinx.serialization.Polymorphic
+import kotlinx.serialization.Serializable
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.dynamicTest
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
+import net.mamoe.mirai.message.data.RichMessage
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.BeforeTest
 import kotlin.test.Test
@@ -80,4 +87,66 @@ internal class RichMessageProtocolTest : AbstractMessageProtocolTest() {
     }
 
     // no encoder. specially handled, no test for now.
+
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+
+    @Serializable
+    data class PolymorphicWrapperRichMessage(
+        override val message: @Polymorphic RichMessage
+    ) : PolymorphicWrapper
+
+    private fun <M : RichMessage> testPolymorphicInRichMessage(
+        data: M,
+        expectedSerialName: String,
+        expectedInstance: M = data,
+    ) = listOf(dynamicTest("testPolymorphicInRichMessage") {
+        testPolymorphicIn(
+            polySerializer = PolymorphicWrapperRichMessage.serializer(),
+            polyConstructor = ::PolymorphicWrapperRichMessage,
+            data = data,
+            expectedSerialName = expectedSerialName,
+            expectedInstance = expectedInstance
+        )
+    })
+
+    @TestFactory
+    fun `test serialization for RichMessage`(): DynamicTestsResult {
+        val data = net.mamoe.mirai.message.data.SimpleServiceMessage(
+            serviceId = 1,
+            content = """
+                    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="1" templateID="123" action="" brief="[分享]ジェリーフィッシュ" sourceMsgId="0" url="https://y.music.163.com/m/song?id=562591636&amp;uct=QK0IOc%2FSCIO8gBNG%2Bwcbsg%3D%3D&amp;app_version=8.7.46" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2" advertiser_id="0" aid="0"><picture cover="http://p1.music.126.net/KaYSb9oYQzhl2XBeJcj8Rg==/109951165125601702.jpg" w="0" h="0" /><title>ジェリーフィッシュ</title><summary>Yunomi/ローラーガール</summary></item><source name="网易云音乐" icon="https://i.gtimg.cn/open/app_icon/00/49/50/85/100495085_100_m.png" action="" a_actionData="tencent100495085://" appid="100495085" /></msg>
+                """.trimIndent()
+        )
+
+        val serialName = net.mamoe.mirai.message.data.SimpleServiceMessage.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInRichMessage(data, serialName),
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+    @TestFactory
+    fun `test serialization for LightApp`(): DynamicTestsResult {
+        val data = net.mamoe.mirai.message.data.LightApp(
+            content = """
+                    <?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="1" templateID="123" action="" brief="[分享]ジェリーフィッシュ" sourceMsgId="0" url="https://y.music.163.com/m/song?id=562591636&amp;uct=QK0IOc%2FSCIO8gBNG%2Bwcbsg%3D%3D&amp;app_version=8.7.46" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2" advertiser_id="0" aid="0"><picture cover="http://p1.music.126.net/KaYSb9oYQzhl2XBeJcj8Rg==/109951165125601702.jpg" w="0" h="0" /><title>ジェリーフィッシュ</title><summary>Yunomi/ローラーガール</summary></item><source name="网易云音乐" icon="https://i.gtimg.cn/open/app_icon/00/49/50/85/100495085_100_m.png" action="" a_actionData="tencent100495085://" appid="100495085" /></msg>
+                """.trimIndent()
+        )
+
+        val serialName = net.mamoe.mirai.message.data.LightApp.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInRichMessage(data, serialName),
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt
index a1a9d47c3..8ea3913bb 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt
@@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl
 
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.At
 import net.mamoe.mirai.message.data.AtAll
 import net.mamoe.mirai.message.data.PlainText
@@ -115,4 +118,51 @@ internal class TextProtocolTest : AbstractMessageProtocolTest() {
             })
         }.doBothChecks()
     }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+    @TestFactory
+    fun `test serialization for PlainText`(): DynamicTestsResult {
+        val data = PlainText(
+            content = """foo""",
+        )
+
+        val serialName = PlainText.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+    @TestFactory
+    fun `test serialization for At`(): DynamicTestsResult {
+        val data = At(
+            100
+        )
+
+        val serialName = At.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
+    @TestFactory
+    fun `test serialization for AtAll`(): DynamicTestsResult {
+        val data = AtAll
+        val serialName = AtAll.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt
index b11b89346..018915ef2 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt
@@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl
 
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.internal.message.protocol.MessageProtocol
+import net.mamoe.mirai.internal.testFramework.DynamicTestsResult
+import net.mamoe.mirai.internal.testFramework.TestFactory
+import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.VipFace
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.BeforeTest
@@ -48,4 +51,24 @@ internal class VipFaceProtocolTest : AbstractMessageProtocolTest() {
             useOrdinaryEquality()
         }.doDecoderChecks()
     }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // serialization
+    ///////////////////////////////////////////////////////////////////////////
+
+    @TestFactory
+    fun `test serialization for VipFace`(): DynamicTestsResult {
+        val data = VipFace(
+            VipFace.LiuLian, 1
+        )
+
+        val serialName = VipFace.SERIAL_NAME
+        return runDynamicTests(
+            testPolymorphicInMessageContent(data, serialName),
+            testPolymorphicInSingleMessage(data, serialName),
+            testInsideMessageChain(data, serialName),
+            testContextual(data, serialName),
+        )
+    }
+
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/serialization/AbstractMessageSerializationTest.kt b/mirai-core/src/commonTest/kotlin/message/serialization/AbstractMessageSerializationTest.kt
new file mode 100644
index 000000000..aa23fd95d
--- /dev/null
+++ b/mirai-core/src/commonTest/kotlin/message/serialization/AbstractMessageSerializationTest.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2019-2022 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.message.serialization
+
+import net.mamoe.mirai.internal.test.AbstractTest
+
+internal abstract class AbstractMessageSerializationTest : AbstractTest()
\ No newline at end of file