Skip to content

Commit

Permalink
Provide support for JsonNamingStrategy to be used in Json for propert…
Browse files Browse the repository at this point in the history
…ies' names. (#2111)

Provide a basic implementation of SnakeCase strategy

Fixes #33
  • Loading branch information
sandwwraith committed Jan 24, 2023
1 parent 694e2f7 commit 60c632c
Show file tree
Hide file tree
Showing 21 changed files with 769 additions and 61 deletions.
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

0 comments on commit 60c632c

Please sign in to comment.