Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide support for JsonNamingStrategy #2111

Merged
merged 5 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package kotlinx.benchmarks.json
import kotlinx.benchmarks.model.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.json.Json.Default.decodeFromString
import kotlinx.serialization.json.Json.Default.encodeToString
import org.openjdk.jmh.annotations.*
import java.util.concurrent.*

Expand All @@ -24,19 +22,25 @@ open class TwitterFeedBenchmark {
*/
private val input = TwitterFeedBenchmark::class.java.getResource("/twitter_macro.json").readBytes().decodeToString()
private val twitter = Json.decodeFromString(MacroTwitterFeed.serializer(), input)

private val jsonNoAltNames = Json { useAlternativeNames = false }
private val jsonIgnoreUnknwn = Json { ignoreUnknownKeys = true }
private val jsonIgnoreUnknwnNoAltNames = Json { ignoreUnknownKeys = true; useAlternativeNames = false}
private val jsonIgnoreUnknwnNoAltNames = Json { ignoreUnknownKeys = true; useAlternativeNames = false }
private val jsonNamingStrategy = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
private val jsonNamingStrategyIgnoreUnknwn = Json(jsonNamingStrategy) { ignoreUnknownKeys = true }

private val twitterKt = jsonNamingStrategy.decodeFromString(MacroTwitterFeedKt.serializer(), input)

@Setup
fun init() {
require(twitter == Json.decodeFromString(MacroTwitterFeed.serializer(), Json.encodeToString(MacroTwitterFeed.serializer(), twitter)))
}

// Order of magnitude: ~400 op/s
// Order of magnitude: ~500 op/s
@Benchmark
fun decodeTwitter() = Json.decodeFromString(MacroTwitterFeed.serializer(), input)

// Should be the same as decodeTwitter, since decodeTwitter never hit unknown name and therefore should never build deserializationNamesMap anyway
@Benchmark
fun decodeTwitterNoAltNames() = jsonNoAltNames.decodeFromString(MacroTwitterFeed.serializer(), input)

Expand All @@ -46,7 +50,20 @@ open class TwitterFeedBenchmark {
@Benchmark
fun decodeMicroTwitter() = jsonIgnoreUnknwn.decodeFromString(MicroTwitterFeed.serializer(), input)

// Should be faster than decodeMicroTwitter, as we explicitly opt-out from deserializationNamesMap on unknown name
@Benchmark
fun decodeMicroTwitterNoAltNames() = jsonIgnoreUnknwnNoAltNames.decodeFromString(MicroTwitterFeed.serializer(), input)

// Should be just a bit slower than decodeMicroTwitter, because alternative names map is created in both cases
@Benchmark
fun decodeMicroTwitterWithNamingStrategy(): MicroTwitterFeedKt = jsonNamingStrategyIgnoreUnknwn.decodeFromString(MicroTwitterFeedKt.serializer(), input)

// Can be slower than decodeTwitter, as we always build deserializationNamesMap when naming strategy is used
@Benchmark
fun decodeTwitterWithNamingStrategy(): MacroTwitterFeedKt = jsonNamingStrategy.decodeFromString(MacroTwitterFeedKt.serializer(), input)

// 15-20% slower than without the strategy. Without serializationNamesMap (invoking strategy on every write), up to 50% slower
@Benchmark
fun encodeTwitterWithNamingStrategy(): String = jsonNamingStrategy.encodeToString(MacroTwitterFeedKt.serializer(), twitterKt)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package kotlinx.benchmarks.model

import kotlinx.serialization.*
import kotlinx.serialization.json.*

/**
* All model classes are the same as in MacroTwitter.kt but named accordingly to Kotlin naming policies to test JsonNamingStrategy performance.
* Only Size, SizeType and Urls are not copied
*/

@Serializable
data class MacroTwitterFeedKt(
val statuses: List<TwitterStatusKt>,
val searchMetadata: SearchMetadata
)

@Serializable
data class MicroTwitterFeedKt(
val statuses: List<TwitterTrimmedStatusKt>
)

@Serializable
data class TwitterTrimmedStatusKt(
val metadata: MetadataKt,
val createdAt: String,
val id: Long,
val idStr: String,
val text: String,
val source: String,
val truncated: Boolean,
val user: TwitterTrimmedUserKt,
val retweetedStatus: TwitterTrimmedStatusKt? = null,
)

@Serializable
data class TwitterStatusKt(
val metadata: MetadataKt,
val createdAt: String,
val id: Long,
val idStr: String,
val text: String,
val source: String,
val truncated: Boolean,
val inReplyToStatusId: Long?,
val inReplyToStatusIdStr: String?,
val inReplyToUserId: Long?,
val inReplyToUserIdStr: String?,
val inReplyToScreenName: String?,
val user: TwitterUserKt,
val geo: String?,
val coordinates: String?,
val place: String?,
val contributors: List<String>?,
val retweetedStatus: TwitterStatusKt? = null,
val retweetCount: Int,
val favoriteCount: Int,
val entities: StatusEntitiesKt,
val favorited: Boolean,
val retweeted: Boolean,
val lang: String,
val possiblySensitive: Boolean? = null
)

@Serializable
data class StatusEntitiesKt(
val hashtags: List<Hashtag>,
val symbols: List<String>,
val urls: List<Url>,
val userMentions: List<TwitterUserMentionKt>,
val media: List<TwitterMediaKt>? = null
)

@Serializable
data class TwitterMediaKt(
val id: Long,
val idStr: String,
val url: String,
val mediaUrl: String,
val mediaUrlHttps: String,
val expandedUrl: String,
val displayUrl: String,
val indices: List<Int>,
val type: String,
val sizes: SizeType,
val sourceStatusId: Long? = null,
val sourceStatusIdStr: String? = null
)

@Serializable
data class TwitterUserMentionKt(
val screenName: String,
val name: String,
val id: Long,
val idStr: String,
val indices: List<Int>
)

@Serializable
data class MetadataKt(
val resultType: String,
val isoLanguageCode: String
)

@Serializable
data class TwitterTrimmedUserKt(
val id: Long,
val idStr: String,
val name: String,
val screenName: String,
val location: String,
val description: String,
val url: String?,
val entities: UserEntitiesKt,
val protected: Boolean,
val followersCount: Int,
val friendsCount: Int,
val listedCount: Int,
val createdAt: String,
val favouritesCount: Int,
)

@Serializable
data class TwitterUserKt(
val id: Long,
val idStr: String,
val name: String,
val screenName: String,
val location: String,
val description: String,
val url: String?,
val entities: UserEntitiesKt,
val protected: Boolean,
val followersCount: Int,
val friendsCount: Int,
val listedCount: Int,
val createdAt: String,
val favouritesCount: Int,
val utcOffset: Int?,
val timeZone: String?,
val geoEnabled: Boolean,
val verified: Boolean,
val statusesCount: Int,
val lang: String,
val contributorsEnabled: Boolean,
val isTranslator: Boolean,
val isTranslationEnabled: Boolean,
val profileBackgroundColor: String,
val profileBackgroundImageUrl: String,
val profileBackgroundImageUrlHttps: String,
val profileBackgroundTile: Boolean,
val profileImageUrl: String,
val profileImageUrlHttps: String,
val profileBannerUrl: String? = null,
val profileLinkColor: String,
val profileSidebarBorderColor: String,
val profileSidebarFillColor: String,
val profileTextColor: String,
val profileUseBackgroundImage: Boolean,
val defaultProfile: Boolean,
val defaultProfileImage: Boolean,
val following: Boolean,
val followRequestSent: Boolean,
val notifications: Boolean
)

@Serializable
data class UserEntitiesKt(
val url: Urls? = null,
val description: Urls
)
3 changes: 3 additions & 0 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ public annotation class Serializer(
* // Prints "{"int":42}"
* println(Json.encodeToString(CustomName(42)))
* ```
*
* If a name of class or property is overridden with this annotation, original source code name is not available for the library.
* Tools like `JsonNamingStrategy` and `ProtoBufSchemaGenerator` would see and transform [value] from [SerialName] annotation.
*/
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
// @Retention(AnnotationRetention.RUNTIME) still runtime, but KT-41082
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ public interface SerialDescriptor {
public fun getElementDescriptor(index: Int): SerialDescriptor

/**
* Whether the element at the given [index] is optional (can be absent is serialized form).
* Whether the element at the given [index] is optional (can be absent in serialized form).
* For generated descriptors, all elements that have a corresponding default parameter value are
* marked as optional. Custom serializers can treat optional values in a serialization-specific manner
* without default parameters constraint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ public abstract class NamedValueDecoder : TaggedDecoder<String>() {
final override fun SerialDescriptor.getTag(index: Int): String = nested(elementName(this, index))

protected fun nested(nestedName: String): String = composeName(currentTagOrNull ?: "", nestedName)
protected open fun elementName(desc: SerialDescriptor, index: Int): String = desc.getElementName(index)
protected open fun elementName(descriptor: SerialDescriptor, index: Int): String = descriptor.getElementName(index)
protected open fun composeName(parentName: String, childName: String): String =
if (parentName.isEmpty()) childName else "$parentName.$childName"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package kotlinx.serialization.features

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlin.test.*

class JsonNamingStrategyExclusionTest : JsonTestBase() {
@SerialInfo
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class OriginalSerialName

private fun List<Annotation>.hasOriginal() = filterIsInstance<OriginalSerialName>().isNotEmpty()

private val myStrategy = JsonNamingStrategy { descriptor, index, serialName ->
if (descriptor.annotations.hasOriginal() || descriptor.getElementAnnotations(index).hasOriginal()) serialName
else JsonNamingStrategy.SnakeCase.serialNameForJson(descriptor, index, serialName)
}

@Serializable
@OriginalSerialName
data class Foo(val firstArg: String = "a", val secondArg: String = "b")

enum class E {
@OriginalSerialName
FIRST_E,
SECOND_E
}

@Serializable
data class Bar(
val firstBar: String = "a",
@OriginalSerialName val secondBar: String = "b",
val fooBar: Foo = Foo(),
val enumBarOne: E = E.FIRST_E,
val enumBarTwo: E = E.SECOND_E
)

private fun doTest(json: Json) {
val j = Json(json) {
namingStrategy = myStrategy
}
val bar = Bar()
assertJsonFormAndRestored(
Bar.serializer(),
bar,
"""{"first_bar":"a","secondBar":"b","foo_bar":{"firstArg":"a","secondArg":"b"},"enum_bar_one":"FIRST_E","enum_bar_two":"SECOND_E"}""",
j
)
}

@Test
fun testJsonNamingStrategyWithAlternativeNames() = doTest(Json(default) {
useAlternativeNames = true
})

@Test
fun testJsonNamingStrategyWithoutAlternativeNames() = doTest(Json(default) {
useAlternativeNames = false
})
}
Loading