diff --git a/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt index 99f7d0a76a..4440836720 100644 --- a/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt +++ b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt @@ -133,3 +133,10 @@ internal constructor(message: String?) : SerializationException(message) { // This constructor is used by the generated serializers constructor(index: Int) : this("An unknown field for index $index") } + +/** + * Thrown when a map deserializer encounters a repeated map key (and configuration disallows this.) + */ +@ExperimentalSerializationApi +public class DuplicateMapKeyException(public val key: Any?) : + SerializationException("Duplicate keys not allowed in maps. Key appeared twice: $key") diff --git a/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt b/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt index dc4aa2ab9e..f701eed86e 100644 --- a/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt +++ b/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt @@ -275,7 +275,7 @@ internal inline fun Decoder.decodeIfNullable(deserializer: Deserializa * [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]. * - * Typically, for unordered data, [CompositeDecoder] is used by a serializer withing a [decodeElementIndex]-based + * Typically, for unordered data, [CompositeDecoder] is used by a serializer within a [decodeElementIndex]-based * loop that decodes all the required data one-by-one in any order and then terminates by calling [endStructure]. * Please refer to [decodeElementIndex] for example of such loop. * @@ -558,6 +558,17 @@ public interface CompositeDecoder { deserializer: DeserializationStrategy, previousValue: T? = null ): T? + + /** + * Called after a key has been read. + * + * This could be a map or set key, or anything otherwise intended to be + * distinct within the collection under normal circumstances. + * + * Implementations might use this as a hook for throwing an exception when + * duplicate keys are encountered. + */ + public fun visitKey(key: Any?) { } } /** diff --git a/core/commonMain/src/kotlinx/serialization/internal/CollectionSerializers.kt b/core/commonMain/src/kotlinx/serialization/internal/CollectionSerializers.kt index fcf4cfa74f..f624c64ec3 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/CollectionSerializers.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/CollectionSerializers.kt @@ -98,6 +98,7 @@ public sealed class MapLikeSerializer() + override fun skipBeginToken() = setSize(decoder.startMap() * 2) + + override fun visitKey(key: Any?) { + val added = seenKeys.add(key) + if (!added) { + throw DuplicateMapKeyException(key) + } + } } private open class CborListReader(cbor: Cbor, decoder: CborDecoder) : CborReader(cbor, decoder) { diff --git a/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborStrictModeTest.kt b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborStrictModeTest.kt new file mode 100644 index 0000000000..43b8ec308b --- /dev/null +++ b/formats/cbor/commonTest/src/kotlinx/serialization/cbor/CborStrictModeTest.kt @@ -0,0 +1,20 @@ +package kotlinx.serialization.cbor + +import kotlinx.serialization.assertFailsWithMessage +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.HexConverter +import kotlinx.serialization.DuplicateMapKeyException +import kotlin.test.Test +import kotlin.test.assertEquals + +class CborStrictModeTest { + + /** Duplicate keys are rejected. */ + @Test + fun testDuplicateKeys() { + val duplicateKeys = HexConverter.parseHexBinary("A2617805617806") + assertFailsWithMessage("Duplicate keys not allowed in maps") { + Cbor.decodeFromByteArray>(duplicateKeys) + } + } +}