Skip to content

Commit 656ba0c

Browse files
authored
Introduce @EncodeDefault annotation with two modes: ALWAYS and NEVER (Kotlin#1528)
Fixes Kotlin#1091
1 parent 67cfed3 commit 656ba0c

File tree

9 files changed

+175
-11
lines changed

9 files changed

+175
-11
lines changed

core/api/kotlinx-serialization-core.api

+11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ public abstract interface class kotlinx/serialization/DeserializationStrategy {
1919
public abstract fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
2020
}
2121

22+
public abstract interface annotation class kotlinx/serialization/EncodeDefault : java/lang/annotation/Annotation {
23+
public abstract fun mode ()Lkotlinx/serialization/EncodeDefault$Mode;
24+
}
25+
26+
public final class kotlinx/serialization/EncodeDefault$Mode : java/lang/Enum {
27+
public static final field ALWAYS Lkotlinx/serialization/EncodeDefault$Mode;
28+
public static final field NEVER Lkotlinx/serialization/EncodeDefault$Mode;
29+
public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/EncodeDefault$Mode;
30+
public static fun values ()[Lkotlinx/serialization/EncodeDefault$Mode;
31+
}
32+
2233
public abstract interface annotation class kotlinx/serialization/ExperimentalSerializationApi : java/lang/annotation/Annotation {
2334
}
2435

core/commonMain/src/kotlinx/serialization/Annotations.kt

+51-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package kotlinx.serialization
88

99
import kotlinx.serialization.descriptors.*
10+
import kotlinx.serialization.encoding.*
1011
import kotlin.reflect.*
1112

1213
/**
@@ -132,6 +133,51 @@ public annotation class Required
132133
// @Retention(AnnotationRetention.RUNTIME) still runtime, but KT-41082
133134
public annotation class Transient
134135

136+
/**
137+
* Controls whether the target property is serialized when its value is equal to a default value,
138+
* regardless of the format settings.
139+
* Does not affect decoding and deserialization process.
140+
*
141+
* Example of usage:
142+
* ```
143+
* @Serializable
144+
* data class Foo(
145+
* @EncodeDefault(ALWAYS) val a: Int = 42,
146+
* @EncodeDefault(NEVER) val b: Int = 43,
147+
* val c: Int = 44
148+
* )
149+
*
150+
* Json { encodeDefaults = false }.encodeToString((Foo()) // {"a": 42}
151+
* Json { encodeDefaults = true }.encodeToString((Foo()) // {"a": 42, "c":44}
152+
* ```
153+
*
154+
* @see EncodeDefault.Mode.ALWAYS
155+
* @see EncodeDefault.Mode.NEVER
156+
*/
157+
@Target(AnnotationTarget.PROPERTY)
158+
@ExperimentalSerializationApi
159+
public annotation class EncodeDefault(val mode: Mode = Mode.ALWAYS) {
160+
/**
161+
* Strategy for the [EncodeDefault] annotation.
162+
*/
163+
@ExperimentalSerializationApi
164+
public enum class Mode {
165+
/**
166+
* Configures serializer to always encode the property, even if its value is equal to its default.
167+
* For annotated properties, format settings are not taken into account and
168+
* [CompositeEncoder.shouldEncodeElementDefault] is not invoked.
169+
*/
170+
ALWAYS,
171+
172+
/**
173+
* Configures serializer not to encode the property if its value is equal to its default.
174+
* For annotated properties, format settings are not taken into account and
175+
* [CompositeEncoder.shouldEncodeElementDefault] is not invoked.
176+
*/
177+
NEVER
178+
}
179+
}
180+
135181
/**
136182
* Meta-annotation that commands the compiler plugin to handle the annotation as serialization-specific.
137183
* Serialization-specific annotations are preserved in the [SerialDescriptor] and can be retrieved
@@ -143,23 +189,23 @@ public annotation class Transient
143189
public annotation class SerialInfo
144190

145191
/**
146-
* Instructs the plugin to use [ContextSerializer] on a given property or type.
192+
* Instructs the plugin to use [ContextualSerializer] on a given property or type.
147193
* Context serializer is usually used when serializer for type can only be found in runtime.
148-
* It is also possible to apply [ContextSerializer] to every property of the given type,
194+
* It is also possible to apply [ContextualSerializer] to every property of the given type,
149195
* using file-level [UseContextualSerialization] annotation.
150196
*
151-
* @see ContextSerializer
197+
* @see ContextualSerializer
152198
* @see UseContextualSerialization
153199
*/
154200
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
155201
@Retention(AnnotationRetention.BINARY)
156202
public annotation class Contextual
157203

158204
/**
159-
* Instructs the plugin to use [ContextSerializer] for every type in the current file that is listed in the [forClasses].
205+
* Instructs the plugin to use [ContextualSerializer] for every type in the current file that is listed in the [forClasses].
160206
*
161207
* @see Contextual
162-
* @see ContextSerializer
208+
* @see ContextualSerializer
163209
*/
164210
@Target(AnnotationTarget.FILE)
165211
@Retention(AnnotationRetention.BINARY)

core/commonMain/src/kotlinx/serialization/encoding/Encoding.kt

+2
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ public interface CompositeEncoder {
348348
* encoder.encodeIntElement(serialDesc, 0, value.int);
349349
* }
350350
* ```
351+
*
352+
* This method is never invoked for properties annotated with [EncodeDefault].
351353
*/
352354
@ExperimentalSerializationApi
353355
public fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = true

formats/json/commonTest/src/kotlinx/serialization/features/SkipDefaults.kt

+26-6
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,64 @@
55
package kotlinx.serialization.features
66

77
import kotlinx.serialization.*
8+
import kotlinx.serialization.EncodeDefault.Mode.*
89
import kotlinx.serialization.json.*
10+
import kotlinx.serialization.test.noLegacyJs
911
import kotlin.test.*
1012

1113
class SkipDefaultsTest {
12-
private val json = Json { encodeDefaults = false }
14+
private val jsonDropDefaults = Json { encodeDefaults = false }
15+
private val jsonEncodeDefaults = Json { encodeDefaults = true }
1316

1417
@Serializable
1518
data class Data(val bar: String, val foo: Int = 42) {
1619
var list: List<Int> = emptyList()
1720
val listWithSomething: List<Int> = listOf(1, 2, 3)
1821
}
1922

23+
@Serializable
24+
data class DifferentModes(
25+
val a: String = "a",
26+
@EncodeDefault val b: String = "b",
27+
@EncodeDefault(ALWAYS) val c: String = "c",
28+
@EncodeDefault(NEVER) val d: String = "d"
29+
)
30+
2031
@Test
2132
fun serializeCorrectlyDefaults() {
22-
val jsonWithDefaults = Json { encodeDefaults = true }
2333
val d = Data("bar")
2434
assertEquals(
2535
"""{"bar":"bar","foo":42,"list":[],"listWithSomething":[1,2,3]}""",
26-
jsonWithDefaults.encodeToString(Data.serializer(), d)
36+
jsonEncodeDefaults.encodeToString(Data.serializer(), d)
2737
)
2838
}
2939

3040
@Test
3141
fun serializeCorrectly() {
3242
val d = Data("bar", 100).apply { list = listOf(1, 2, 3) }
33-
assertEquals("""{"bar":"bar","foo":100,"list":[1,2,3]}""", json.encodeToString(Data.serializer(), d))
43+
assertEquals(
44+
"""{"bar":"bar","foo":100,"list":[1,2,3]}""",
45+
jsonDropDefaults.encodeToString(Data.serializer(), d)
46+
)
3447
}
3548

3649
@Test
3750
fun serializeCorrectlyAndDropBody() {
3851
val d = Data("bar", 43)
39-
assertEquals("""{"bar":"bar","foo":43}""", json.encodeToString(Data.serializer(), d))
52+
assertEquals("""{"bar":"bar","foo":43}""", jsonDropDefaults.encodeToString(Data.serializer(), d))
4053
}
4154

4255
@Test
4356
fun serializeCorrectlyAndDropAll() {
4457
val d = Data("bar")
45-
assertEquals("""{"bar":"bar"}""", json.encodeToString(Data.serializer(), d))
58+
assertEquals("""{"bar":"bar"}""", jsonDropDefaults.encodeToString(Data.serializer(), d))
59+
}
60+
61+
@Test
62+
fun encodeDefaultsAnnotationWithFlag() = noLegacyJs {
63+
val data = DifferentModes()
64+
assertEquals("""{"a":"a","b":"b","c":"c"}""", jsonEncodeDefaults.encodeToString(data))
65+
assertEquals("""{"b":"b","c":"c"}""", jsonDropDefaults.encodeToString(data))
4666
}
4767

4868
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.protobuf
6+
7+
import kotlinx.serialization.*
8+
import kotlinx.serialization.test.isJsLegacy
9+
import kotlin.test.*
10+
11+
class ProtobufNullAndDefaultTest {
12+
@Serializable
13+
class ProtoWithNullDefault(val s: String? = null)
14+
15+
@Serializable
16+
class ProtoWithNullDefaultAlways(@EncodeDefault val s: String? = null)
17+
18+
@Serializable
19+
class ProtoWithNullDefaultNever(@EncodeDefault(EncodeDefault.Mode.NEVER) val s: String? = null)
20+
21+
@Test
22+
fun testProtobufDropDefaults() {
23+
val proto = ProtoBuf { encodeDefaults = false }
24+
assertEquals(0, proto.encodeToByteArray(ProtoWithNullDefault()).size)
25+
if (isJsLegacy()) return // because of @EncodeDefault
26+
assertFailsWith<SerializationException> { proto.encodeToByteArray(ProtoWithNullDefaultAlways()) }
27+
assertEquals(0, proto.encodeToByteArray(ProtoWithNullDefaultNever()).size)
28+
}
29+
30+
@Test
31+
fun testProtobufEncodeDefaults() {
32+
val proto = ProtoBuf { encodeDefaults = true }
33+
assertFailsWith<SerializationException> { proto.encodeToByteArray(ProtoWithNullDefault()) }
34+
if (isJsLegacy()) return // because of @EncodeDefault
35+
assertFailsWith<SerializationException> { proto.encodeToByteArray(ProtoWithNullDefaultAlways()) }
36+
assertEquals(0, proto.encodeToByteArray(ProtoWithNullDefaultNever()).size)
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.test
6+
7+
enum class Platform {
8+
JVM, JS_LEGACY, JS_IR, NATIVE
9+
}
10+
11+
public expect val currentPlatform: Platform
12+
13+
public fun isJs(): Boolean = currentPlatform == Platform.JS_LEGACY || currentPlatform == Platform.JS_IR
14+
public fun isJsLegacy(): Boolean = currentPlatform == Platform.JS_LEGACY
15+
public fun isJvm(): Boolean = currentPlatform == Platform.JVM
16+
public fun isNative(): Boolean = currentPlatform == Platform.NATIVE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.test
6+
7+
public actual val currentPlatform: Platform = if (isLegacyBackend()) Platform.JS_LEGACY else Platform.JS_IR
8+
9+
// from https://github.com/JetBrains/kotlin/blob/569187a7516e2e5ab217158a3170d4beb0c5cb5a/js/js.translator/testData/_commonFiles/testUtils.kt#L3
10+
private fun isLegacyBackend(): Boolean =
11+
// Using eval to prevent DCE from thinking that following code depends on Kotlin module.
12+
eval("(typeof Kotlin != \"undefined\" && typeof Kotlin.kotlin != \"undefined\")").unsafeCast<Boolean>()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.test
6+
7+
actual val currentPlatform: Platform = Platform.JVM
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.test
6+
7+
8+
import kotlin.native.concurrent.SharedImmutable
9+
10+
11+
@SharedImmutable
12+
public actual val currentPlatform: Platform = Platform.NATIVE

0 commit comments

Comments
 (0)