From 2673d168d8dbcb58d54fa5c621c598ec8899fd09 Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Thu, 10 Apr 2025 21:24:37 +0100 Subject: [PATCH 1/6] Add a slightly more comprehensive (and common) implementation of the default EfficientBinaryFormat, and add it to the parametrizedTest. It contains some workarounds to handle json format specifics. It implements stringformat by recording the binary serialization as shadow value. --- .../ByteReadingBuffer.kt | 68 ++++++++++ .../ByteWritingBuffer.kt | 89 +++++++++++++ .../EfficientBinaryFormat.kt | 118 ++++++++++++++++++ .../serialization/json/JsonTestBase.kt | 58 ++++++++- 4 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteWritingBuffer.kt create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt new file mode 100644 index 0000000000..91c3989492 --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.efficientBinaryFormat + +class ByteReadingBuffer(val buffer: ByteArray) { + private var next = 0 + + private fun nextByte(): Int { + return buffer[next++].toInt() and 0xff + } + + private fun nextByteL(): Long { + return buffer[next++].toLong() and 0xffL + } + + operator fun get(pos: Int): Byte { + if(pos !in 0.. encodeToByteArray( + serializer: SerializationStrategy, + value: T + ): ByteArray { + val encoder = Encoder(serializersModule) + serializer.serialize(encoder, value) + return encoder.byteBuffer.toByteArray() + } + + override fun decodeFromByteArray( + deserializer: DeserializationStrategy, + bytes: ByteArray + ): T { + val decoder = Decoder(serializersModule, bytes) + return deserializer.deserialize(decoder) + } + + class Encoder(override val serializersModule: SerializersModule): AbstractEncoder() { + val byteBuffer = ByteWritingBuffer() + override fun encodeBoolean(value: Boolean) = byteBuffer.writeByte(if (value) 1 else 0) + override fun encodeByte(value: Byte) = byteBuffer.writeByte(value) + override fun encodeShort(value: Short) = byteBuffer.writeShort(value) + override fun encodeInt(value: Int) = byteBuffer.writeInt(value) + override fun encodeLong(value: Long) = byteBuffer.writeLong(value) + override fun encodeFloat(value: Float) = byteBuffer.writeFloat(value) + override fun encodeDouble(value: Double) = byteBuffer.writeDouble(value) + override fun encodeChar(value: Char) = byteBuffer.writeChar(value) + override fun encodeString(value: String) = byteBuffer.writeString(value) + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = byteBuffer.writeInt(index) + + @ExperimentalSerializationApi + override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = true + + override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder { + encodeInt(collectionSize) + return this + } + + override fun encodeNull() = encodeBoolean(false) + override fun encodeNotNullMark() = encodeBoolean(true) + + } + + class Decoder(override val serializersModule: SerializersModule, private val reader: ByteReadingBuffer) : AbstractDecoder() { + + constructor(serializersModule: SerializersModule, bytes: ByteArray) : this( + serializersModule, + ByteReadingBuffer(bytes) + ) + + private var nextElementIndex = 0 +// private var currentDesc: SerialDescriptor? = null + + override fun decodeBoolean(): Boolean = reader.readByte().toInt() != 0 + + override fun decodeByte(): Byte = reader.readByte() + + override fun decodeShort(): Short = reader.readShort() + + override fun decodeInt(): Int = reader.readInt() + + override fun decodeLong(): Long = reader.readLong() + + override fun decodeFloat(): Float = reader.readFloat() + + override fun decodeDouble(): Double = reader.readDouble() + + override fun decodeChar(): Char = reader.readChar() + + override fun decodeString(): String = reader.readString() + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = reader.readInt() + + override fun decodeNotNullMark(): Boolean = decodeBoolean() + + @ExperimentalSerializationApi + override fun decodeSequentially(): Boolean = true + + override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = reader.readInt() + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + return Decoder(serializersModule, reader) + } + + override fun endStructure(descriptor: SerialDescriptor) { + check(nextElementIndex ==0 || descriptor.elementsCount == nextElementIndex) { "Type: ${descriptor.serialName} not fully read: ${descriptor.elementsCount} != $nextElementIndex" } + } + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + return when (nextElementIndex) { + descriptor.elementsCount -> CompositeDecoder.DECODE_DONE + else -> nextElementIndex++ + } + } + } +} \ No newline at end of file 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 de8cfb38b0..7a603594f8 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt @@ -6,6 +6,7 @@ package kotlinx.serialization.json import kotlinx.io.* import kotlinx.serialization.* +import kotlinx.serialization.efficientBinaryFormat.EfficientBinaryFormat import kotlinx.serialization.json.internal.* import kotlinx.serialization.json.io.* import kotlinx.serialization.json.okio.decodeFromBufferedSource @@ -129,12 +130,67 @@ abstract class JsonTestBase { } } + /** A test runner that effectively handles the json tests to also test serialization to + * "efficient" binary. This mainly checks serializer implementations. + */ + private inner class EfficientBinary( + val json: Json, + val ebf: EfficientBinaryFormat = EfficientBinaryFormat(), + ) : StringFormat { + override val serializersModule: SerializersModule = ebf.serializersModule + + private var bytes: ByteArray? = null + private var jsonStr: String? = null + + @OptIn(ExperimentalStdlibApi::class) + override fun encodeToString(serializer: SerializationStrategy, value: T): String { + bytes = runCatching { ebf.encodeToByteArray(serializer, value) } + .onFailure { if ("Json format" !in it.message!!) throw it } + .getOrNull() + return json.encodeToString(serializer, value).also { + if (bytes != null) jsonStr = it + } + } + + @OptIn(ExperimentalStdlibApi::class) + override fun decodeFromString(deserializer: DeserializationStrategy, string: String): T { + /* + * to retain compatibility with json we support different cases. If + * the string has been encoded already use that. Instead, if the + * deserializer is also a serializer (the default) then use that to + * get the value from json and encode that to bytes which are then + * decoded. In this case capture and ignore cases that require a + * json encoder. + * + * Finally fall back to json decoding (nothing can be done) + */ + + var bytes = this@EfficientBinary.bytes + if (string == jsonStr && bytes != null) { + return ebf.decodeFromByteArray(deserializer, bytes) + } else if (deserializer is SerializationStrategy<*>) { + val value = json.decodeFromString(deserializer, string) + // + @Suppress("UNCHECKED_CAST") + runCatching { ebf.encodeToByteArray(deserializer as SerializationStrategy, value) }.onSuccess { r -> + bytes = r + jsonStr = string + return ebf.decodeFromByteArray(deserializer, bytes) + }.onFailure { e -> + if ("Json format" !in e.message!!) throw e + } + } + return json.decodeFromString(deserializer, string) + } + } + protected fun parametrizedTest(json: Json, test: StringFormat.() -> Unit) { val streamingResult = runCatching { SwitchableJson(json, JsonTestingMode.STREAMING).test() } val treeResult = runCatching { SwitchableJson(json, JsonTestingMode.TREE).test() } val okioResult = runCatching { SwitchableJson(json, JsonTestingMode.OKIO_STREAMS).test() } val kxioResult = runCatching { SwitchableJson(json, JsonTestingMode.KXIO_STREAMS).test() } - processResults(listOf(streamingResult, treeResult, okioResult, kxioResult)) + val efficientBinaryResult = runCatching { EfficientBinary(json).test() } + processResults(listOf(streamingResult, treeResult, okioResult, kxioResult, efficientBinaryResult)) } protected fun processResults(results: List>) { From 1b6b2dd9fc54c24cf488de1613a84627bafabb7c Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Tue, 15 Apr 2025 17:48:50 +0100 Subject: [PATCH 2/6] Add ChunkedDecoder support --- .../efficientBinaryFormat/ByteReadingBuffer.kt | 12 ++++++++++++ .../efficientBinaryFormat/EfficientBinaryFormat.kt | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt index 91c3989492..4c31719f0e 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/ByteReadingBuffer.kt @@ -65,4 +65,16 @@ class ByteReadingBuffer(val buffer: ByteArray) { return chars.concatToString() } + fun readString(consumeChunk: (String) -> Unit) { + val len = readInt() + var remaining = len + while (remaining > 1024) { + remaining -= 1024 + val chunk = CharArray(1024) { readChar() } + consumeChunk(chunk.concatToString()) + } + val chars = CharArray(remaining) { readChar() } + consumeChunk(chars.concatToString()) + } + } \ No newline at end of file diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt index 192a9287eb..5e9f4130fd 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.encoding.ChunkedDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.CompositeEncoder import kotlinx.serialization.modules.EmptySerializersModule @@ -63,7 +64,7 @@ class EfficientBinaryFormat( } - class Decoder(override val serializersModule: SerializersModule, private val reader: ByteReadingBuffer) : AbstractDecoder() { + class Decoder(override val serializersModule: SerializersModule, private val reader: ByteReadingBuffer) : AbstractDecoder(), ChunkedDecoder { constructor(serializersModule: SerializersModule, bytes: ByteArray) : this( serializersModule, @@ -91,6 +92,11 @@ class EfficientBinaryFormat( override fun decodeString(): String = reader.readString() + @ExperimentalSerializationApi + override fun decodeStringChunked(consumeChunk: (String) -> Unit) { + reader.readString(consumeChunk) + } + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = reader.readInt() override fun decodeNotNullMark(): Boolean = decodeBoolean() From f27a2e69436ca66e0c41604bd92dbd1b070ebaa1 Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Tue, 15 Apr 2025 19:28:05 +0100 Subject: [PATCH 3/6] Support reordering of element writing (in pessimisation case) --- .../EfficientBinaryFormat.kt | 88 ++++++++++++++++--- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt index 5e9f4130fd..b059e5368c 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/efficientBinaryFormat/EfficientBinaryFormat.kt @@ -38,25 +38,87 @@ class EfficientBinaryFormat( return deserializer.deserialize(decoder) } - class Encoder(override val serializersModule: SerializersModule): AbstractEncoder() { - val byteBuffer = ByteWritingBuffer() - override fun encodeBoolean(value: Boolean) = byteBuffer.writeByte(if (value) 1 else 0) - override fun encodeByte(value: Byte) = byteBuffer.writeByte(value) - override fun encodeShort(value: Short) = byteBuffer.writeShort(value) - override fun encodeInt(value: Int) = byteBuffer.writeInt(value) - override fun encodeLong(value: Long) = byteBuffer.writeLong(value) - override fun encodeFloat(value: Float) = byteBuffer.writeFloat(value) - override fun encodeDouble(value: Double) = byteBuffer.writeDouble(value) - override fun encodeChar(value: Char) = byteBuffer.writeChar(value) - override fun encodeString(value: String) = byteBuffer.writeString(value) - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = byteBuffer.writeInt(index) + class Encoder( + override val serializersModule: SerializersModule, + internal val byteBuffer: ByteWritingBuffer = ByteWritingBuffer(), + elementsCount: Int = -1 + ): AbstractEncoder() { + var lastWrittenIndex = -1 + var currentIndex = -1 + val notInStruct = elementsCount < 0 + + val pending : Array<(() -> Unit)?> = when { + elementsCount <=0 -> emptyArray() + else -> arrayOfNulls(elementsCount) + } + + override fun encodeBoolean(value: Boolean) = writeOrSuspend { byteBuffer.writeByte(if (value) 1 else 0) } + override fun encodeByte(value: Byte) = writeOrSuspend { byteBuffer.writeByte(value) } + override fun encodeShort(value: Short) = writeOrSuspend { byteBuffer.writeShort(value) } + override fun encodeInt(value: Int) = writeOrSuspend { byteBuffer.writeInt(value) } + override fun encodeLong(value: Long) = writeOrSuspend { byteBuffer.writeLong(value) } + override fun encodeFloat(value: Float) = writeOrSuspend { byteBuffer.writeFloat(value) } + override fun encodeDouble(value: Double) = writeOrSuspend { byteBuffer.writeDouble(value) } + override fun encodeChar(value: Char) = writeOrSuspend { byteBuffer.writeChar(value) } + override fun encodeString(value: String) = writeOrSuspend { byteBuffer.writeString(value) } + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = writeOrSuspend { + byteBuffer.writeInt(index) + } + + @ExperimentalSerializationApi + override fun encodeNullableSerializableValue( + serializer: SerializationStrategy, + value: T? + ) { + writeOrSuspend { + super.encodeNullableSerializableValue(serializer, value) + } + } + + override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { + writeOrSuspend { + super.encodeSerializableValue(serializer, value) + } + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun writeOrSuspend(noinline action: () -> Unit) { + val c = currentIndex + currentIndex = -1 + when { + notInStruct || c<0 -> action() + lastWrittenIndex < -1 -> pending[c] = action + lastWrittenIndex + 1 == c -> { + ++lastWrittenIndex + action() + } + c < pending.size -> pending[c] = action + else -> error("Unexpected index") + } + } + + override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + currentIndex = index + return true + } @ExperimentalSerializationApi override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = true + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + return Encoder(serializersModule, byteBuffer, descriptor.elementsCount) + } + override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder { encodeInt(collectionSize) - return this + return Encoder(serializersModule, byteBuffer, -1) + } + + override fun endStructure(descriptor: SerialDescriptor) { + currentIndex = -2 // mark negative to ensure writing + for (i in 0 until pending.size) { + pending[i]?.invoke() + } } override fun encodeNull() = encodeBoolean(false) From c1e314ae1f2917c129fb52809a399d4f206b39cb Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Tue, 15 Apr 2025 20:18:07 +0100 Subject: [PATCH 4/6] Introduce JsonTestingMode.EFFICIENT_BINARY as mode that involves a roundtrip through the EfficientBinaryFormat. --- .../json/JsonErrorMessagesTest.kt | 4 +-- .../serialization/json/JsonTestBase.kt | 36 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) 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 da73bc3a21..1042bbf9ef 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonErrorMessagesTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonErrorMessagesTest.kt @@ -131,7 +131,7 @@ class JsonErrorMessagesTest : JsonTestBase() { // it can be some kind of path `{"foo:bar:baz":"my:resource:locator:{123}"}` or even URI used as a string key/value. // So if the closing quote is missing, there's really no way to correctly tell where the key or value is supposed to end. // Although we may try to unify these messages for consistency. - if (mode in setOf(JsonTestingMode.STREAMING, JsonTestingMode.TREE)) + if (mode in setOf(JsonTestingMode.STREAMING, JsonTestingMode.TREE, JsonTestingMode.EFFICIENT_BINARY)) assertContains( message, "Unexpected JSON token at offset 7: Expected quotation mark '\"', but had ':' instead at path: \$" @@ -145,7 +145,7 @@ class JsonErrorMessagesTest : JsonTestBase() { checkSerializationException({ default.decodeFromString(serString, input2, mode) }, { message -> - if (mode in setOf(JsonTestingMode.STREAMING, JsonTestingMode.TREE)) + if (mode in setOf(JsonTestingMode.STREAMING, JsonTestingMode.TREE, JsonTestingMode.EFFICIENT_BINARY)) assertContains( message, "Unexpected JSON token at offset 13: Expected quotation mark '\"', but had '}' instead at path: \$" 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 7a603594f8..9df48fdde7 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt @@ -26,7 +26,8 @@ enum class JsonTestingMode { TREE, OKIO_STREAMS, JAVA_STREAMS, - KXIO_STREAMS; + KXIO_STREAMS, + EFFICIENT_BINARY; companion object { fun value(i: Int) = values()[i] @@ -42,6 +43,7 @@ abstract class JsonTestBase { return encodeToString(serializer, value, jsonTestingMode) } + @OptIn(ExperimentalStdlibApi::class) internal fun Json.encodeToString( serializer: SerializationStrategy, value: T, @@ -68,6 +70,18 @@ abstract class JsonTestBase { encodeToSink(serializer, value, buffer) buffer.readString() } + JsonTestingMode.EFFICIENT_BINARY -> { + val ebf = EfficientBinaryFormat() + val bytes = runCatching { ebf.encodeToByteArray(serializer, value) }.getOrElse { e-> + null//throw e + } + if (bytes != null && serializer is KSerializer<*>) { + val decoded = ebf.decodeFromByteArray((serializer as KSerializer), bytes) + encodeToString(serializer, decoded) + } else { + encodeToString(serializer, value) + } + } } internal inline fun Json.decodeFromString(source: String, jsonTestingMode: JsonTestingMode): T { @@ -75,6 +89,7 @@ abstract class JsonTestBase { return decodeFromString(deserializer, source, jsonTestingMode) } + @OptIn(ExperimentalStdlibApi::class) internal fun Json.decodeFromString( deserializer: DeserializationStrategy, source: String, @@ -101,6 +116,20 @@ abstract class JsonTestBase { buffer.writeString(source) decodeFromSource(deserializer, buffer) } + JsonTestingMode.EFFICIENT_BINARY -> { + when (deserializer){ + is KSerializer<*> -> { + val s = deserializer as KSerializer + val value = decodeFromString(deserializer, source) + runCatching { + val ebf = EfficientBinaryFormat() + val binaryValue = EfficientBinaryFormat().encodeToByteArray(s, value) + ebf.decodeFromByteArray(s, binaryValue) + }.getOrElse { value } + } + else -> decodeFromString(deserializer, source) + } + } } protected open fun parametrizedTest(test: (JsonTestingMode) -> Unit) { @@ -145,7 +174,10 @@ abstract class JsonTestBase { @OptIn(ExperimentalStdlibApi::class) override fun encodeToString(serializer: SerializationStrategy, value: T): String { bytes = runCatching { ebf.encodeToByteArray(serializer, value) } - .onFailure { if ("Json format" !in it.message!!) throw it } + .onFailure { if ("Json format" !in it.message!!) { + json.encodeToString(serializer, value) // trigger throwing the json exception if the exception is there + throw it + } } .getOrNull() return json.encodeToString(serializer, value).also { if (bytes != null) jsonStr = it From 707c6c847029c92d5f34301639f471c36684789b Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Tue, 15 Apr 2025 20:19:09 +0100 Subject: [PATCH 5/6] Update the test base to test other forms of parametrizedTest. --- .../commonTest/src/kotlinx/serialization/json/JsonTestBase.kt | 2 ++ 1 file changed, 2 insertions(+) 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 9df48fdde7..d7a264b0b3 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt @@ -138,6 +138,8 @@ abstract class JsonTestBase { add(runCatching { test(JsonTestingMode.TREE) }) add(runCatching { test(JsonTestingMode.OKIO_STREAMS) }) add(runCatching { test(JsonTestingMode.KXIO_STREAMS) }) + add(runCatching { test(JsonTestingMode.EFFICIENT_BINARY) } + .recover { e -> if ("Json format" in (e.message ?: "")) Unit else throw e }) if (isJvm()) { add(runCatching { test(JsonTestingMode.JAVA_STREAMS) }) From 1c82bde40ef86d338c310344ad5de63b5c6ee453 Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Tue, 15 Apr 2025 20:21:03 +0100 Subject: [PATCH 6/6] Fix the custom serializers with EfficientBinaryFormat by making them consider shouldEncodeElementDefault's output. --- .../json/JsonCustomSerializersTest.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt index 351866df9f..35f0c320b1 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt @@ -41,7 +41,7 @@ class JsonCustomSerializersTest : JsonTestBase() { override fun serialize(encoder: Encoder, value: C) { val elemOutput = encoder.beginStructure(descriptor) elemOutput.encodeIntElement(descriptor, 1, value.b) - if (value.a != 31) elemOutput.encodeIntElement(descriptor, 0, value.a) + if (value.a != 31 || elemOutput.shouldEncodeElementDefault(descriptor, 0)) elemOutput.encodeIntElement(descriptor, 0, value.a) elemOutput.endStructure(descriptor) } } @@ -57,7 +57,9 @@ class JsonCustomSerializersTest : JsonTestBase() { override fun serialize(encoder: Encoder, value: CList2) { val elemOutput = encoder.beginStructure(descriptor) elemOutput.encodeSerializableElement(descriptor, 1, ListSerializer(C), value.c) - if (value.d != 5) elemOutput.encodeIntElement(descriptor, 0, value.d) + if (value.d != 5 || elemOutput.shouldEncodeElementDefault(descriptor, 0)) { + elemOutput.encodeIntElement(descriptor, 0, value.d) + } elemOutput.endStructure(descriptor) } } @@ -69,7 +71,9 @@ class JsonCustomSerializersTest : JsonTestBase() { companion object : KSerializer { override fun serialize(encoder: Encoder, value: CList3) { val elemOutput = encoder.beginStructure(descriptor) - if (value.e.isNotEmpty()) elemOutput.encodeSerializableElement(descriptor, 0, ListSerializer(C), value.e) + if (value.e.isNotEmpty() || elemOutput.shouldEncodeElementDefault(descriptor, 0)) { + elemOutput.encodeSerializableElement(descriptor, 0, ListSerializer(C), value.e) + } elemOutput.encodeIntElement(descriptor, 1, value.f) elemOutput.endStructure(descriptor) } @@ -83,7 +87,9 @@ class JsonCustomSerializersTest : JsonTestBase() { override fun serialize(encoder: Encoder, value: CList4) { val elemOutput = encoder.beginStructure(descriptor) elemOutput.encodeIntElement(descriptor, 1, value.h) - if (value.g.isNotEmpty()) elemOutput.encodeSerializableElement(descriptor, 0, ListSerializer(C), value.g) + if (value.g.isNotEmpty() || elemOutput.shouldEncodeElementDefault(descriptor, 0)) { + elemOutput.encodeSerializableElement(descriptor, 0, ListSerializer(C), value.g) + } elemOutput.endStructure(descriptor) } } @@ -96,10 +102,12 @@ class JsonCustomSerializersTest : JsonTestBase() { override fun serialize(encoder: Encoder, value: CList5) { val elemOutput = encoder.beginStructure(descriptor) elemOutput.encodeIntElement(descriptor, 1, value.h) - if (value.g.isNotEmpty()) elemOutput.encodeSerializableElement( - descriptor, 0, ListSerializer(Int.serializer()), - value.g - ) + if (value.g.isNotEmpty() || elemOutput.shouldEncodeElementDefault(descriptor, 0)) { + elemOutput.encodeSerializableElement( + descriptor, 0, ListSerializer(Int.serializer()), + value.g + ) + } elemOutput.endStructure(descriptor) } }