Skip to content

Commit

Permalink
Introduce @EncodeDefault annotation with two modes: ALWAYS and NEVER (#…
Browse files Browse the repository at this point in the history
…1528)

Fixes #1091
  • Loading branch information
sandwwraith authored Aug 12, 2021
1 parent 67cfed3 commit 656ba0c
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 11 deletions.
11 changes: 11 additions & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ public abstract interface class kotlinx/serialization/DeserializationStrategy {
public abstract fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
}

public abstract interface annotation class kotlinx/serialization/EncodeDefault : java/lang/annotation/Annotation {
public abstract fun mode ()Lkotlinx/serialization/EncodeDefault$Mode;
}

public final class kotlinx/serialization/EncodeDefault$Mode : java/lang/Enum {
public static final field ALWAYS Lkotlinx/serialization/EncodeDefault$Mode;
public static final field NEVER Lkotlinx/serialization/EncodeDefault$Mode;
public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/EncodeDefault$Mode;
public static fun values ()[Lkotlinx/serialization/EncodeDefault$Mode;
}

public abstract interface annotation class kotlinx/serialization/ExperimentalSerializationApi : java/lang/annotation/Annotation {
}

Expand Down
56 changes: 51 additions & 5 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package kotlinx.serialization

import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlin.reflect.*

/**
Expand Down Expand Up @@ -132,6 +133,51 @@ public annotation class Required
// @Retention(AnnotationRetention.RUNTIME) still runtime, but KT-41082
public annotation class Transient

/**
* Controls whether the target property is serialized when its value is equal to a default value,
* regardless of the format settings.
* Does not affect decoding and deserialization process.
*
* Example of usage:
* ```
* @Serializable
* data class Foo(
* @EncodeDefault(ALWAYS) val a: Int = 42,
* @EncodeDefault(NEVER) val b: Int = 43,
* val c: Int = 44
* )
*
* Json { encodeDefaults = false }.encodeToString((Foo()) // {"a": 42}
* Json { encodeDefaults = true }.encodeToString((Foo()) // {"a": 42, "c":44}
* ```
*
* @see EncodeDefault.Mode.ALWAYS
* @see EncodeDefault.Mode.NEVER
*/
@Target(AnnotationTarget.PROPERTY)
@ExperimentalSerializationApi
public annotation class EncodeDefault(val mode: Mode = Mode.ALWAYS) {
/**
* Strategy for the [EncodeDefault] annotation.
*/
@ExperimentalSerializationApi
public enum class Mode {
/**
* Configures serializer to always encode the property, even if its value is equal to its default.
* For annotated properties, format settings are not taken into account and
* [CompositeEncoder.shouldEncodeElementDefault] is not invoked.
*/
ALWAYS,

/**
* Configures serializer not to encode the property if its value is equal to its default.
* For annotated properties, format settings are not taken into account and
* [CompositeEncoder.shouldEncodeElementDefault] is not invoked.
*/
NEVER
}
}

/**
* Meta-annotation that commands the compiler plugin to handle the annotation as serialization-specific.
* Serialization-specific annotations are preserved in the [SerialDescriptor] and can be retrieved
Expand All @@ -143,23 +189,23 @@ public annotation class Transient
public annotation class SerialInfo

/**
* Instructs the plugin to use [ContextSerializer] on a given property or type.
* Instructs the plugin to use [ContextualSerializer] on a given property or type.
* Context serializer is usually used when serializer for type can only be found in runtime.
* It is also possible to apply [ContextSerializer] to every property of the given type,
* It is also possible to apply [ContextualSerializer] to every property of the given type,
* using file-level [UseContextualSerialization] annotation.
*
* @see ContextSerializer
* @see ContextualSerializer
* @see UseContextualSerialization
*/
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
public annotation class Contextual

/**
* Instructs the plugin to use [ContextSerializer] for every type in the current file that is listed in the [forClasses].
* Instructs the plugin to use [ContextualSerializer] for every type in the current file that is listed in the [forClasses].
*
* @see Contextual
* @see ContextSerializer
* @see ContextualSerializer
*/
@Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.BINARY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ public interface CompositeEncoder {
* encoder.encodeIntElement(serialDesc, 0, value.int);
* }
* ```
*
* This method is never invoked for properties annotated with [EncodeDefault].
*/
@ExperimentalSerializationApi
public fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,64 @@
package kotlinx.serialization.features

import kotlinx.serialization.*
import kotlinx.serialization.EncodeDefault.Mode.*
import kotlinx.serialization.json.*
import kotlinx.serialization.test.noLegacyJs
import kotlin.test.*

class SkipDefaultsTest {
private val json = Json { encodeDefaults = false }
private val jsonDropDefaults = Json { encodeDefaults = false }
private val jsonEncodeDefaults = Json { encodeDefaults = true }

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

@Serializable
data class DifferentModes(
val a: String = "a",
@EncodeDefault val b: String = "b",
@EncodeDefault(ALWAYS) val c: String = "c",
@EncodeDefault(NEVER) val d: String = "d"
)

@Test
fun serializeCorrectlyDefaults() {
val jsonWithDefaults = Json { encodeDefaults = true }
val d = Data("bar")
assertEquals(
"""{"bar":"bar","foo":42,"list":[],"listWithSomething":[1,2,3]}""",
jsonWithDefaults.encodeToString(Data.serializer(), d)
jsonEncodeDefaults.encodeToString(Data.serializer(), d)
)
}

@Test
fun serializeCorrectly() {
val d = Data("bar", 100).apply { list = listOf(1, 2, 3) }
assertEquals("""{"bar":"bar","foo":100,"list":[1,2,3]}""", json.encodeToString(Data.serializer(), d))
assertEquals(
"""{"bar":"bar","foo":100,"list":[1,2,3]}""",
jsonDropDefaults.encodeToString(Data.serializer(), d)
)
}

@Test
fun serializeCorrectlyAndDropBody() {
val d = Data("bar", 43)
assertEquals("""{"bar":"bar","foo":43}""", json.encodeToString(Data.serializer(), d))
assertEquals("""{"bar":"bar","foo":43}""", jsonDropDefaults.encodeToString(Data.serializer(), d))
}

@Test
fun serializeCorrectlyAndDropAll() {
val d = Data("bar")
assertEquals("""{"bar":"bar"}""", json.encodeToString(Data.serializer(), d))
assertEquals("""{"bar":"bar"}""", jsonDropDefaults.encodeToString(Data.serializer(), d))
}

@Test
fun encodeDefaultsAnnotationWithFlag() = noLegacyJs {
val data = DifferentModes()
assertEquals("""{"a":"a","b":"b","c":"c"}""", jsonEncodeDefaults.encodeToString(data))
assertEquals("""{"b":"b","c":"c"}""", jsonDropDefaults.encodeToString(data))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.protobuf

import kotlinx.serialization.*
import kotlinx.serialization.test.isJsLegacy
import kotlin.test.*

class ProtobufNullAndDefaultTest {
@Serializable
class ProtoWithNullDefault(val s: String? = null)

@Serializable
class ProtoWithNullDefaultAlways(@EncodeDefault val s: String? = null)

@Serializable
class ProtoWithNullDefaultNever(@EncodeDefault(EncodeDefault.Mode.NEVER) val s: String? = null)

@Test
fun testProtobufDropDefaults() {
val proto = ProtoBuf { encodeDefaults = false }
assertEquals(0, proto.encodeToByteArray(ProtoWithNullDefault()).size)
if (isJsLegacy()) return // because of @EncodeDefault
assertFailsWith<SerializationException> { proto.encodeToByteArray(ProtoWithNullDefaultAlways()) }
assertEquals(0, proto.encodeToByteArray(ProtoWithNullDefaultNever()).size)
}

@Test
fun testProtobufEncodeDefaults() {
val proto = ProtoBuf { encodeDefaults = true }
assertFailsWith<SerializationException> { proto.encodeToByteArray(ProtoWithNullDefault()) }
if (isJsLegacy()) return // because of @EncodeDefault
assertFailsWith<SerializationException> { proto.encodeToByteArray(ProtoWithNullDefaultAlways()) }
assertEquals(0, proto.encodeToByteArray(ProtoWithNullDefaultNever()).size)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.test

enum class Platform {
JVM, JS_LEGACY, JS_IR, NATIVE
}

public expect val currentPlatform: Platform

public fun isJs(): Boolean = currentPlatform == Platform.JS_LEGACY || currentPlatform == Platform.JS_IR
public fun isJsLegacy(): Boolean = currentPlatform == Platform.JS_LEGACY
public fun isJvm(): Boolean = currentPlatform == Platform.JVM
public fun isNative(): Boolean = currentPlatform == Platform.NATIVE
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.test

public actual val currentPlatform: Platform = if (isLegacyBackend()) Platform.JS_LEGACY else Platform.JS_IR

// from https://github.com/JetBrains/kotlin/blob/569187a7516e2e5ab217158a3170d4beb0c5cb5a/js/js.translator/testData/_commonFiles/testUtils.kt#L3
private fun isLegacyBackend(): Boolean =
// Using eval to prevent DCE from thinking that following code depends on Kotlin module.
eval("(typeof Kotlin != \"undefined\" && typeof Kotlin.kotlin != \"undefined\")").unsafeCast<Boolean>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.test

actual val currentPlatform: Platform = Platform.JVM
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.test


import kotlin.native.concurrent.SharedImmutable


@SharedImmutable
public actual val currentPlatform: Platform = Platform.NATIVE

0 comments on commit 656ba0c

Please sign in to comment.