forked from Kotlin/kotlinx.serialization
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allows to set
ProtoBuf.shouldEncodeElementDefault
to false
Porting a project using the *Java ProtoBuf library* to *kotlinx.serialization* can become quite hard since optional properties are not supported in the same way. On the one hand it is not possible to omit optional properties during serialization (this is possible by not setting an optional property while building the message in Java). On the other hand it is not possible to check if a property has been omitted by the sender during de-serialization (this is possible via `Message.hasA()` in Java). Allowing to set `ProtoBuf.shouldEncodeElementDefault()` to `false` allows to create `@Serializable data classes` that support optional properties during serialization and de-serialization. Additionally, it makes `null` values possible for optional properties as well. Closes Kotlin#397 Make skipping null values default behavior for protobuf Closes Kotlin#71 NULLs are not supported when writing to protobuf
- Loading branch information
Sven Obser
committed
Jan 9, 2020
1 parent
75061d3
commit 5f7b7db
Showing
6 changed files
with
415 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
242 changes: 242 additions & 0 deletions
242
runtime/jvmTest/src/kotlinx/serialization/formats/protobuf/ProtoBufNullTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
/* | ||
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.serialization.formats.protobuf | ||
|
||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.dump | ||
import kotlinx.serialization.formats.IMessage | ||
import kotlinx.serialization.formats.dumpCompare | ||
import kotlinx.serialization.formats.proto.TestData | ||
import kotlinx.serialization.formats.readCompare | ||
import kotlinx.serialization.load | ||
import kotlinx.serialization.protobuf.ProtoBuf | ||
import org.junit.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertFalse | ||
import kotlin.test.assertTrue | ||
|
||
/** | ||
* Test that [ProtoBuf] works correctly if [ProtoBuf.encodeDefaults] is set to `false` | ||
* and that `null` is allowed as a default value. | ||
* | ||
* In this case `null` values should not get encoded into bytes. This allows to check if an optional | ||
* property was set or not (like it is possible with the *Java ProtoBuf library*). | ||
*/ | ||
class ProtoBufNullTest { | ||
|
||
/** ProtoBuf instance that does **not** encode defaults. */ | ||
private val protoBuf = ProtoBuf(encodeDefaults = false) | ||
|
||
@Test | ||
fun readCompareWithNulls() { | ||
val data = MessageWithOptionals() | ||
assertTrue(readCompare(data, alwaysPrint = true, protoBuf = protoBuf)) | ||
} | ||
|
||
@Test | ||
fun dumpCompareWithNulls() { | ||
val data = MessageWithOptionals() | ||
assertTrue(dumpCompare(data, alwaysPrint = true, protoBuf = protoBuf)) | ||
} | ||
|
||
@Test | ||
fun readCompareWithDefaults() { | ||
val data = MessageWithOptionals(0, "", MessageWithOptionals.Position.FIRST, 99, listOf(1, 2, 3)) | ||
assertTrue(readCompare(data, alwaysPrint = true, protoBuf = protoBuf)) | ||
} | ||
|
||
@Test | ||
fun dumpCompareWithDefaults() { | ||
val data = MessageWithOptionals(0, "", MessageWithOptionals.Position.FIRST, 99, listOf(1, 2, 3)) | ||
assertTrue(dumpCompare(data, alwaysPrint = true, protoBuf = protoBuf)) | ||
} | ||
|
||
@Test | ||
fun readCompareWithValues() { | ||
val data = MessageWithOptionals(42, "Test", MessageWithOptionals.Position.SECOND, 24, listOf(1, 2, 3)) | ||
assertTrue(readCompare(data, alwaysPrint = true, protoBuf = protoBuf)) | ||
} | ||
|
||
@Test | ||
fun dumpCompareWithValues() { | ||
val data = MessageWithOptionals(42, "Test", MessageWithOptionals.Position.SECOND, 24, listOf(1, 2, 3)) | ||
assertTrue(dumpCompare(data, alwaysPrint = true, protoBuf = protoBuf)) | ||
} | ||
|
||
@Test | ||
fun testThatNullValuesAreNotEncoded() { | ||
val data = MessageWithOptionals() | ||
val parsed = TestData.MessageWithOptionals.parseFrom(protoBuf.dump(data)) | ||
|
||
assertFalse(parsed.hasA(), "Expected that null value for optional property `a` is not encoded.") | ||
assertFalse(parsed.hasB(), "Expected that null value for optional property `b` is not encoded.") | ||
assertFalse(parsed.hasC(), "Expected that null value for optional property `c` is not encoded.") | ||
assertFalse(parsed.hasD(), "Expected that null value for optional property `d` is not encoded.") | ||
|
||
assertEquals(0, parsed.a, "Expected default value for property `a`.") | ||
assertEquals("", parsed.b, "Expected default value for property `b`.") | ||
assertEquals(TestData.MessageWithOptionals.Position.FIRST, parsed.c, "Expected default value for property `c`.") | ||
assertEquals(99, parsed.d, "Expected default value for property `d`.") | ||
assertEquals(emptyList(), parsed.eList, "Expected default value for property `e`.") | ||
} | ||
|
||
@Test | ||
fun testThatDefaultValuesAreEncodedCorrectly() { | ||
val data = MessageWithOptionals(0, "", MessageWithOptionals.Position.FIRST, 99, emptyList()) | ||
val parsed = TestData.MessageWithOptionals.parseFrom(protoBuf.dump(data)) | ||
|
||
assertTrue(parsed.hasA(), "Expected that custom value for optional property `a` is encoded.") | ||
assertTrue(parsed.hasB(), "Expected that custom value for optional property `b` is encoded.") | ||
assertTrue(parsed.hasC(), "Expected that custom value for optional property `c` is encoded.") | ||
assertTrue(parsed.hasD(), "Expected that custom value for optional property `d` is encoded.") | ||
|
||
assertEquals(0, parsed.a, "Expected custom value for property `a`.") | ||
assertEquals("", parsed.b, "Expected custom value for property `b`.") | ||
assertEquals(TestData.MessageWithOptionals.Position.FIRST, parsed.c, "Expected custom value for property `c`.") | ||
assertEquals(99, parsed.d, "Expected custom value for property `d`.") | ||
assertEquals(emptyList(), parsed.eList, "Expected custom value for property `e`.") | ||
} | ||
|
||
@Test | ||
fun testThatCustomValuesAreEncodedCorrectly() { | ||
val data = MessageWithOptionals(42, "Test", MessageWithOptionals.Position.SECOND, 24, listOf(1, 2, 3)) | ||
val parsed = TestData.MessageWithOptionals.parseFrom(protoBuf.dump(data)) | ||
|
||
assertTrue(parsed.hasA(), "Expected that custom value for optional property `a` is encoded.") | ||
assertTrue(parsed.hasB(), "Expected that custom value for optional property `b` is encoded.") | ||
assertTrue(parsed.hasC(), "Expected that custom value for optional property `c` is encoded.") | ||
assertTrue(parsed.hasD(), "Expected that custom value for optional property `d` is encoded.") | ||
|
||
assertEquals(42, parsed.a, "Expected custom value for property `a`.") | ||
assertEquals("Test", parsed.b, "Expected custom value for property `b`.") | ||
assertEquals(TestData.MessageWithOptionals.Position.SECOND, parsed.c, "Expected custom value for property `c`.") | ||
assertEquals(24, parsed.d, "Expected custom value for property `d`.") | ||
assertEquals(listOf(1, 2, 3), parsed.eList, "Expected custom value for property `e`.") | ||
} | ||
|
||
@Test | ||
fun testThatNullValuesAreNotDecoded() { | ||
val data = TestData.MessageWithOptionals.newBuilder().build() | ||
val parsed = protoBuf.load<MessageWithOptionals>(data.toByteArray()) | ||
|
||
assertFalse(parsed.hasA(), "Expected that null value for optional property `a` is not decoded.") | ||
assertFalse(parsed.hasB(), "Expected that null value for optional property `b` is not decoded.") | ||
assertFalse(parsed.hasC(), "Expected that null value for optional property `c` is not decoded.") | ||
assertFalse(parsed.hasD(), "Expected that null value for optional property `d` is not decoded.") | ||
|
||
assertEquals(0, parsed.a, "Expected default value for property `a`.") | ||
assertEquals("", parsed.b, "Expected default value for property `b`.") | ||
assertEquals(MessageWithOptionals.Position.FIRST, parsed.c, "Expected default value for property `c`.") | ||
assertEquals(99, parsed.d, "Expected default value for property `d`.") | ||
assertEquals(emptyList(), parsed.e, "Expected default value for property `e`.") | ||
} | ||
|
||
@Test | ||
fun testThatDefaultValuesAreDecodedCorrectly() { | ||
val data = TestData.MessageWithOptionals.newBuilder() | ||
.setA(0) | ||
.setB("") | ||
.setC(TestData.MessageWithOptionals.Position.FIRST) | ||
.setD(99) | ||
.addAllE(emptyList()) | ||
.build() | ||
val parsed = protoBuf.load<MessageWithOptionals>(data.toByteArray()) | ||
|
||
assertTrue(parsed.hasA(), "Expected that custom value for optional property `a` is decoded.") | ||
assertTrue(parsed.hasB(), "Expected that custom value for optional property `b` is decoded.") | ||
assertTrue(parsed.hasC(), "Expected that custom value for optional property `c` is decoded.") | ||
assertTrue(parsed.hasD(), "Expected that custom value for optional property `d` is decoded.") | ||
|
||
assertEquals(0, parsed.a, "Expected custom value for property `a`.") | ||
assertEquals("", parsed.b, "Expected custom value for property `b`.") | ||
assertEquals(MessageWithOptionals.Position.FIRST, parsed.c, "Expected custom value for property `c`.") | ||
assertEquals(99, parsed.d, "Expected custom value for property `d`.") | ||
assertEquals(emptyList(), parsed.e, "Expected custom value for property `e`.") | ||
} | ||
|
||
@Test | ||
fun testThatCustomValuesAreDecodedCorrectly() { | ||
val data = TestData.MessageWithOptionals.newBuilder() | ||
.setA(42) | ||
.setB("Test") | ||
.setC(TestData.MessageWithOptionals.Position.SECOND) | ||
.setD(24) | ||
.addAllE(listOf(1, 2, 3)) | ||
.build() | ||
val parsed = protoBuf.load<MessageWithOptionals>(data.toByteArray()) | ||
|
||
assertTrue(parsed.hasA(), "Expected that custom value for optional property `a` is decoded.") | ||
assertTrue(parsed.hasB(), "Expected that custom value for optional property `b` is decoded.") | ||
assertTrue(parsed.hasC(), "Expected that custom value for optional property `c` is decoded.") | ||
assertTrue(parsed.hasD(), "Expected that custom value for optional property `d` is decoded.") | ||
|
||
assertEquals(42, parsed.a, "Expected custom value for property `a`.") | ||
assertEquals("Test", parsed.b, "Expected custom value for property `b`.") | ||
assertEquals(MessageWithOptionals.Position.SECOND, parsed.c, "Expected custom value for property `c`.") | ||
assertEquals(24, parsed.d, "Expected custom value for property `d`.") | ||
assertEquals(listOf(1, 2, 3), parsed.e, "Expected custom value for property `e`.") | ||
} | ||
|
||
/** | ||
* Test [Serializable] that manually implements `TestOptional` defined in `test_data.proto`. | ||
* | ||
* Using `null` as default values allows to implement [hasA], ... according to Java ProtoBuf library. | ||
*/ | ||
@Serializable | ||
private data class MessageWithOptionals( | ||
private val _a: Int? = null, | ||
private val _b: String? = null, | ||
private val _c: Position? = null, | ||
private val _d: Int? = null, | ||
private val _e: List<Int>? = null | ||
) : IMessage { | ||
|
||
val a: Int | ||
get() = _a ?: 0 | ||
|
||
val b: String | ||
get() = _b ?: "" | ||
|
||
val c: Position | ||
get() = _c ?: Position.FIRST | ||
|
||
val d: Int | ||
get() = _d ?: 99 | ||
|
||
val e: List<Int> | ||
get() = _e ?: emptyList() | ||
|
||
fun hasA() = _a != null | ||
|
||
fun hasB() = _b != null | ||
|
||
fun hasC() = _c != null | ||
|
||
fun hasD() = _d != null | ||
|
||
/** | ||
* Convert this [Serializable] object to its expected [TestData.MessageWithOptionals] ProtoBuf message. | ||
* | ||
* For this test we expect that `null` values are not encoded. | ||
*/ | ||
override fun toProtobufMessage(): TestData.MessageWithOptionals = | ||
TestData.MessageWithOptionals.newBuilder().also { builder -> | ||
if (_a != null) builder.a = _a | ||
if (_b != null) builder.b = _b | ||
if (_c != null) builder.c = _c.toProtoBuf() | ||
if (_d != null) builder.d = _d | ||
if (_e != null) builder.addAllE(_e) | ||
}.build() | ||
|
||
enum class Position { | ||
FIRST, SECOND; | ||
|
||
fun toProtoBuf() = when (this) { | ||
FIRST -> TestData.MessageWithOptionals.Position.FIRST | ||
SECOND -> TestData.MessageWithOptionals.Position.SECOND | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.