diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/TwitterFeedBenchmark.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/TwitterFeedBenchmark.kt index 22ae35f5a7..e015ad96ab 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/TwitterFeedBenchmark.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/TwitterFeedBenchmark.kt @@ -24,6 +24,9 @@ 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} @Setup fun init() { @@ -34,6 +37,16 @@ open class TwitterFeedBenchmark { @Benchmark fun decodeTwitter() = Json.decodeFromString(MacroTwitterFeed.serializer(), input) + @Benchmark + fun decodeTwitterNoAltNames() = jsonNoAltNames.decodeFromString(MacroTwitterFeed.serializer(), input) + @Benchmark fun encodeTwitter() = Json.encodeToString(MacroTwitterFeed.serializer(), twitter) + + @Benchmark + fun decodeMicroTwitter() = jsonIgnoreUnknwn.decodeFromString(MicroTwitterFeed.serializer(), input) + + @Benchmark + fun decodeMicroTwitterNoAltNames() = jsonIgnoreUnknwnNoAltNames.decodeFromString(MicroTwitterFeed.serializer(), input) + } diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/model/MacroTwitter.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/model/MacroTwitter.kt index 8bbb933ac1..d52607fa97 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/model/MacroTwitter.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/model/MacroTwitter.kt @@ -9,6 +9,24 @@ data class MacroTwitterFeed( val search_metadata: SearchMetadata ) +@Serializable +data class MicroTwitterFeed( + val statuses: List +) + +@Serializable +data class TwitterReducedStatus( + val metadata: Metadata, + val created_at: String, + val id: Long, + val id_str: String, + val text: String, + val source: String, + val truncated: Boolean, + val user: TwitterReducedUser, + val retweeted_status: TwitterReducedStatus? = null, +) + @Serializable data class TwitterStatus( val metadata: Metadata, @@ -92,6 +110,24 @@ data class Metadata( val iso_language_code: String ) +@Serializable +data class TwitterReducedUser( + val id: Long, + val id_str: String, + val name: String, + val screen_name: String, + val location: String, + val description: String, + val url: String?, + val entities: UserEntities, + val protected: Boolean, + val followers_count: Int, + val friends_count: Int, + val listed_count: Int, + val created_at: String, + val favourites_count: Int, +) + @Serializable data class TwitterUser( val id: Long, diff --git a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt index 71017d3407..278e6ed846 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/PluginGeneratedSerialDescriptor.kt @@ -97,8 +97,8 @@ internal open class PluginGeneratedSerialDescriptor( override fun hashCode(): Int = _hashCode override fun toString(): String { - return indices.entries.joinToString(", ", "$serialName(", ")") { - it.key + ": " + getElementDescriptor(it.value).serialName + return (0 until elementsCount).joinToString(", ", "$serialName(", ")") { i -> + getElementName(i) + ": " + getElementDescriptor(i).serialName } } } diff --git a/docs/json.md b/docs/json.md index 84689c6c68..e37e6c23a2 100644 --- a/docs/json.md +++ b/docs/json.md @@ -13,6 +13,7 @@ In this chapter we'll walk through various [Json] features. * [Pretty printing](#pretty-printing) * [Lenient parsing](#lenient-parsing) * [Ignoring unknown keys](#ignoring-unknown-keys) + * [Alternative Json names](#alternative-json-names) * [Coercing input values](#coercing-input-values) * [Encoding defaults](#encoding-defaults) * [Allowing structured map keys](#allowing-structured-map-keys) @@ -151,6 +152,40 @@ Project(name=kotlinx.serialization) +### Alternative Json names + +It's not a rare case when JSON fields got renamed due to a schema version change or something else. +Renaming JSON fields is available with [`@SerialName` annotation](basic-serialization.md#serial-field-names), but +such a renaming blocks ability to decode data with old name. +For the case when we want to support multiple JSON names for the one Kotlin property, there is a [JsonNames] annotation: + +```kotlin +@Serializable +data class Project(@JsonNames(["title"]) val name: String) + +fun main() { + val project = Json.decodeFromString("""{"name":"kotlinx.serialization"}""") + println(project) + val oldProject = Json.decodeFromString("""{"title":"kotlinx.coroutines"}""") + println(oldProject) +} +``` + +> You can get the full code [here](../guide/example/example-json-04.kt). + +As you can see, both `name` and `title` Json fields correspond to `name` property: + +```text +Project(name=kotlinx.serialization) +Project(name=kotlinx.coroutines) +``` + +Support for [JsonNames] annotation is controlled via [JsonBuilder.useAlternativeNames] flag. +Unlike most of the configuration flags, this one is enabled by default and does not need attention +unless you want to do some fine-tuning. + + + ### Coercing input values JSON formats that are encountered in the wild can be flexible in terms of types and evolve quickly. @@ -185,7 +220,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-04.kt). +> You can get the full code [here](../guide/example/example-json-05.kt). We see that invalid `null` value for the `language` property was coerced into the default value. @@ -219,7 +254,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-05.kt). +> You can get the full code [here](../guide/example/example-json-06.kt). It produces the following output which encodes the values of all the properties: @@ -251,7 +286,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-06.kt). +> You can get the full code [here](../guide/example/example-json-07.kt). The map with structured keys gets represented as `[key1, value1, key2, value2,...]` JSON array. @@ -282,7 +317,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-07.kt). +> You can get the full code [here](../guide/example/example-json-08.kt). This example produces the following non-stardard JSON output, yet it is a widely used encoding for special values in JVM world. @@ -316,7 +351,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-08.kt). +> You can get the full code [here](../guide/example/example-json-09.kt). In combination with an explicitly specified [SerialName] of the class it provides full control on the resulting JSON object. @@ -348,7 +383,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-09.kt). +> You can get the full code [here](../guide/example/example-json-10.kt). A `JsonElement` prints itself as a valid JSON. @@ -391,7 +426,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-10.kt). +> You can get the full code [here](../guide/example/example-json-11.kt). The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`, but failing if the structure of the data is otherwise different. @@ -430,7 +465,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-11.kt). +> You can get the full code [here](../guide/example/example-json-12.kt). At the end, we get a proper JSON string. @@ -459,7 +494,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-12.kt). +> You can get the full code [here](../guide/example/example-json-13.kt). The result is exactly what we would expect. @@ -536,7 +571,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-13.kt). +> You can get the full code [here](../guide/example/example-json-14.kt). The output shows that both cases are correctly deserialized into a Kotlin [List]. @@ -588,7 +623,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-14.kt). +> You can get the full code [here](../guide/example/example-json-15.kt). We end up with a single JSON object. @@ -633,7 +668,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-15.kt). +> You can get the full code [here](../guide/example/example-json-16.kt). We can clearly see the effect of the custom serializer. @@ -706,7 +741,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-16.kt). +> You can get the full code [here](../guide/example/example-json-17.kt). No class discriminator is added in the JSON output. @@ -802,7 +837,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-17.kt). +> You can get the full code [here](../guide/example/example-json-18.kt). This gives us fine-grained control on the representation of the `Response` class in our JSON output. @@ -867,7 +902,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-18.kt). +> You can get the full code [here](../guide/example/example-json-19.kt). ```text UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"}) @@ -904,6 +939,8 @@ The next chapter covers [Alternative and custom formats (experimental)](formats. [JsonBuilder.prettyPrint]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/index.html#kotlinx.serialization.json%2FJsonBuilder%2FprettyPrint%2F%23%2FPointingToDeclaration%2F [JsonBuilder.isLenient]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/index.html#kotlinx.serialization.json%2FJsonBuilder%2FisLenient%2F%23%2FPointingToDeclaration%2F [JsonBuilder.ignoreUnknownKeys]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/index.html#kotlinx.serialization.json%2FJsonBuilder%2FignoreUnknownKeys%2F%23%2FPointingToDeclaration%2F +[JsonNames]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-names/index.html +[JsonBuilder.useAlternativeNames]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/index.html#kotlinx.serialization.json%2FJsonBuilder%2FuseAlternativeNames%2F%23%2FPointingToDeclaration%2F [JsonBuilder.coerceInputValues]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/index.html#kotlinx.serialization.json%2FJsonBuilder%2FcoerceInputValues%2F%23%2FPointingToDeclaration%2F [JsonBuilder.encodeDefaults]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/index.html#kotlinx.serialization.json%2FJsonBuilder%2FencodeDefaults%2F%23%2FPointingToDeclaration%2F [JsonBuilder.allowStructuredMapKeys]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/index.html#kotlinx.serialization.json%2FJsonBuilder%2FallowStructuredMapKeys%2F%23%2FPointingToDeclaration%2F diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index 1b7e270ce1..d631860ae4 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -106,6 +106,7 @@ Once the project is set up, we can start serializing some classes. * [Pretty printing](json.md#pretty-printing) * [Lenient parsing](json.md#lenient-parsing) * [Ignoring unknown keys](json.md#ignoring-unknown-keys) + * [Alternative Json names](json.md#alternative-json-names) * [Coercing input values](json.md#coercing-input-values) * [Encoding defaults](json.md#encoding-defaults) * [Allowing structured map keys](json.md#allowing-structured-map-keys) diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api index d71a2de394..0b4940c644 100644 --- a/formats/json/api/kotlinx-serialization-json.api +++ b/formats/json/api/kotlinx-serialization-json.api @@ -83,6 +83,7 @@ public final class kotlinx/serialization/json/JsonBuilder { public final fun getPrettyPrint ()Z public final fun getPrettyPrintIndent ()Ljava/lang/String; public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; + public final fun getUseAlternativeNames ()Z public final fun getUseArrayPolymorphism ()Z public final fun isLenient ()Z public final fun setAllowSpecialFloatingPointValues (Z)V @@ -95,6 +96,7 @@ public final class kotlinx/serialization/json/JsonBuilder { public final fun setPrettyPrint (Z)V public final fun setPrettyPrintIndent (Ljava/lang/String;)V public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V + public final fun setUseAlternativeNames (Z)V public final fun setUseArrayPolymorphism (Z)V } @@ -190,6 +192,15 @@ public final class kotlinx/serialization/json/JsonKt { public static synthetic fun Json$default (Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/Json; } +public abstract interface annotation class kotlinx/serialization/json/JsonNames : java/lang/annotation/Annotation { + public abstract fun names ()[Ljava/lang/String; +} + +public final class kotlinx/serialization/json/JsonNames$Impl : kotlinx/serialization/json/JsonNames { + public fun ([Ljava/lang/String;)V + public final fun names ()[Ljava/lang/String; +} + public final class kotlinx/serialization/json/JsonNull : kotlinx/serialization/json/JsonPrimitive { public static final field INSTANCE Lkotlinx/serialization/json/JsonNull; public fun getContent ()Ljava/lang/String; diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt index d237c800e9..807209549e 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt @@ -7,6 +7,7 @@ package kotlinx.serialization.json import kotlinx.serialization.* import kotlinx.serialization.json.internal.* import kotlinx.serialization.modules.* +import kotlin.native.concurrent.* /** * The main entry point to work with JSON serialization. @@ -52,9 +53,12 @@ public sealed class Json(internal val configuration: JsonConfiguration) : String override val serializersModule: SerializersModule get() = configuration.serializersModule + internal val schemaCache: DescriptorSchemaCache = DescriptorSchemaCache() + /** * The default instance of [Json] with default configuration. */ + @ThreadLocal // to support caching public companion object Default : Json(JsonConfiguration()) /** @@ -228,6 +232,14 @@ public class JsonBuilder internal constructor(configuration: JsonConfiguration) */ public var allowSpecialFloatingPointValues: Boolean = configuration.allowSpecialFloatingPointValues + /** + * Switches whether Json instance make use of [JsonNames] annotation; enabled by default. + * + * Disabling this flag when one do not use [JsonNames] at all may sometimes result in better performance, + * particularly when a large count of fields is skipped with [ignoreUnknownKeys]. + */ + public var useAlternativeNames: Boolean = configuration.useAlternativeNames + /** * Module with contextual and polymorphic serializers to be used in the resulting [Json] instance. */ @@ -255,7 +267,8 @@ public class JsonBuilder internal constructor(configuration: JsonConfiguration) encodeDefaults, ignoreUnknownKeys, isLenient, allowStructuredMapKeys, prettyPrint, prettyPrintIndent, coerceInputValues, useArrayPolymorphism, - classDiscriminator, allowSpecialFloatingPointValues, serializersModule + classDiscriminator, allowSpecialFloatingPointValues, useAlternativeNames, + serializersModule ) } } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNames.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNames.kt new file mode 100644 index 0000000000..e40be3fbdd --- /dev/null +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNames.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.json + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.json.internal.* +import kotlin.native.concurrent.* + +/** + * Specifies an array of names those can be treated as alternative possible names + * for the property during JSON decoding. Unlike [SerialName], does not affect JSON + * encoding in any way. + * + * This annotation has lesser priority than [SerialName], even if there is a collision between them. + * + * @see JsonBuilder.useAlternativeNames + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +@ExperimentalSerializationApi +public annotation class JsonNames(val names: Array) + +@SharedImmutable +internal val JsonAlternativeNamesKey = DescriptorSchemaCache.Key>() + +@OptIn(ExperimentalSerializationApi::class) +internal fun SerialDescriptor.buildAlternativeNamesMap(): Map { + fun MutableMap.putOrThrow(name: String, index: Int) { + if (name in this) { + throw JsonException( + "The suggested name '$name' for property ${getElementName(index)} is already one of the names for property " + + "${getElementName(getValue(name))} in ${this@buildAlternativeNamesMap}" + ) + } + this[name] = index + } + + var builder: MutableMap? = null + for (i in 0 until elementsCount) { + getElementAnnotations(i).filterIsInstance().singleOrNull()?.names?.forEach { name -> + if (builder == null) builder = createMapForCache(elementsCount) + builder!!.putOrThrow(name, i) + } + } + return builder ?: emptyMap() +} diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt index 1c9f3210f7..318e091eca 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt @@ -21,5 +21,6 @@ internal data class JsonConfiguration( @JvmField public val useArrayPolymorphism: Boolean = false, @JvmField public val classDiscriminator: String = "type", @JvmField public val allowSpecialFloatingPointValues: Boolean = false, + @JvmField public val useAlternativeNames: Boolean = true, @JvmField public val serializersModule: SerializersModule = EmptySerializersModule ) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/SchemaCache.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/SchemaCache.kt new file mode 100644 index 0000000000..de65fb6867 --- /dev/null +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/SchemaCache.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.json.internal + +import kotlinx.serialization.descriptors.* +import kotlin.native.concurrent.* + +private typealias DescriptorData = MutableMap, T> + +/** + * A type-safe map for storing custom information (such as format schema), associated with [SerialDescriptor]. + * + * This cache uses ConcurrentHashMap on JVM and regular maps on other platforms. + * To be able to work with it from multiple threads in Kotlin/Native, use @[ThreadLocal] in appropriate places. + */ +internal class DescriptorSchemaCache { + private val map: MutableMap> = createMapForCache(1) + + @Suppress("UNCHECKED_CAST") + public operator fun set(descriptor: SerialDescriptor, key: Key, value: T) { + map.getOrPut(descriptor, { createMapForCache(1) })[key as Key] = value as Any + } + + public fun getOrPut(descriptor: SerialDescriptor, key: Key, defaultValue: () -> T): T { + get(descriptor, key)?.let { return it } + val value = defaultValue() + set(descriptor, key, value) + return value + } + + @Suppress("UNCHECKED_CAST") + public operator fun get(descriptor: SerialDescriptor, key: Key): T? { + return map[descriptor]?.get(key as Key) as? T + } + + /** + * A key for associating user data of type [T] with a given [SerialDescriptor]. + */ + public class Key {} +} + + +/** + * Creates a ConcurrentHashMap on JVM and regular HashMap on other platforms. + * To make actual use of cache in Kotlin/Native, mark a top-level object with this map + * as a @[ThreadLocal]. + */ +internal expect fun createMapForCache(initialCapacity: Int): MutableMap diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt index 03c4f8f3c1..71143c2770 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt @@ -101,6 +101,19 @@ internal open class StreamingJsonDecoder( } } + private fun SerialDescriptor.getJsonElementIndex(key: String): Int { + val index = this.getElementIndex(key) + // Fast path, do not go through ConcurrentHashMap.get + // Note, it blocks ability to detect collisions between the primary name and alternate, + // but it eliminates a significant performance penalty (about -15% without this optimization) + if (index != UNKNOWN_NAME) return index + if (!json.configuration.useAlternativeNames) return index + // Slow path + val alternativeNamesMap = + json.schemaCache.getOrPut(this, JsonAlternativeNamesKey, this::buildAlternativeNamesMap) + return alternativeNamesMap[key] ?: UNKNOWN_NAME + } + /* * Checks whether JSON has `null` value for non-null property or unknown enum value for enum property */ @@ -127,7 +140,7 @@ internal open class StreamingJsonDecoder( hasComma = false val key = decodeStringKey() lexer.consumeNextToken(COLON) - val index = descriptor.getElementIndex(key) + val index = descriptor.getJsonElementIndex(key) val isUnknown = if (index != UNKNOWN_NAME) { if (configuration.coerceInputValues && coerceInputValue(descriptor, index)) { hasComma = lexer.tryConsumeComma() diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt index 217e0cc988..5d02c5b9be 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt @@ -209,6 +209,20 @@ private open class JsonTreeDecoder( return CompositeDecoder.DECODE_DONE } + override fun elementName(desc: SerialDescriptor, index: Int): String { + val mainName = desc.getElementName(index) + if (!configuration.useAlternativeNames) return mainName + // Fast path, do not go through ConcurrentHashMap.get + // Note, it blocks ability to detect collisions between the primary name and alternate, + // but it eliminates a significant performance penalty (about -15% without this optimization) + if (mainName in value.keys) return mainName + // Slow path + val alternativeNamesMap = + json.schemaCache.getOrPut(desc, JsonAlternativeNamesKey, desc::buildAlternativeNamesMap) + val nameInObject = value.keys.find { it == mainName || alternativeNamesMap[it] == index } + return nameInObject ?: mainName + } + override fun currentElement(tag: String): JsonElement = value.getValue(tag) override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { @@ -224,7 +238,12 @@ private open class JsonTreeDecoder( if (configuration.ignoreUnknownKeys || descriptor.kind is PolymorphicKind) return // Validate keys @Suppress("DEPRECATION_ERROR") - val names = descriptor.jsonCachedSerialNames() + val names: Set = + if (!configuration.useAlternativeNames) + descriptor.jsonCachedSerialNames() + else + descriptor.jsonCachedSerialNames() + json.schemaCache[descriptor, JsonAlternativeNamesKey]?.keys.orEmpty() + for (key in value.keys) { if (key !in names && key != polyDiscriminator) { throw UnknownKeyException(key, value.toString()) diff --git a/formats/json/commonTest/src/kotlinx/serialization/features/JsonAlternativeNamesTest.kt b/formats/json/commonTest/src/kotlinx/serialization/features/JsonAlternativeNamesTest.kt new file mode 100644 index 0000000000..81b2bcdffe --- /dev/null +++ b/formats/json/commonTest/src/kotlinx/serialization/features/JsonAlternativeNamesTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("ReplaceArrayOfWithLiteral") // https://youtrack.jetbrains.com/issue/KT-22578 + +package kotlinx.serialization.features + +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlinx.serialization.test.* +import kotlin.test.* + +class JsonAlternativeNamesTest : JsonTestBase() { + + @Serializable + data class WithNames(@JsonNames(arrayOf("foo", "_foo")) val data: String) + + @Serializable + data class CollisionWithAlternate( + @JsonNames(arrayOf("_foo")) val data: String, + @JsonNames(arrayOf("_foo")) val foo: String + ) + + private val inputString1 = """{"foo":"foo"}""" + private val inputString2 = """{"_foo":"foo"}""" + private val json = Json { useAlternativeNames = true } + + @Test + fun testParsesAllAlternativeNames() { + for (input in listOf(inputString1, inputString2)) { + for (streaming in listOf(true, false)) { + val data = json.decodeFromString(WithNames.serializer(), input, useStreaming = streaming) + assertEquals("foo", data.data, "Failed to parse input '$input' with streaming=$streaming") + } + } + } + + private fun doThrowTest( + expectedErrorMessage: String, + serializer: KSerializer, + input: String + ) = + parametrizedTest { streaming -> + assertFailsWithMessage( + expectedErrorMessage, + "Class ${serializer.descriptor.serialName} did not fail with streaming=$streaming" + ) { + json.decodeFromString(serializer, input, useStreaming = streaming) + } + } + + @Test + fun testThrowsAnErrorOnDuplicateNames2() = doThrowTest( + """The suggested name '_foo' for property foo is already one of the names for property data""", + CollisionWithAlternate.serializer(), + inputString2 + ) +} diff --git a/formats/json/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt b/formats/json/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt index d3fc33c5e8..a515c0b3e1 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/json/JsonCustomSerializersTest.kt @@ -113,7 +113,10 @@ class JsonCustomSerializersTest : JsonTestBase() { private val moduleWithB = serializersModuleOf(B::class, BSerializer) - private fun createJsonWithB() = Json { isLenient = true; serializersModule = moduleWithB } + private fun createJsonWithB() = Json { isLenient = true; serializersModule = moduleWithB; useAlternativeNames = false } + // useAlternativeNames uses SerialDescriptor.hashCode, + // which is unavailable for partially-customized serializers such as in this file + private val jsonNoAltNames = Json { useAlternativeNames = false } @Test fun testWriteCustom() = parametrizedTest { useStreaming -> @@ -166,35 +169,35 @@ class JsonCustomSerializersTest : JsonTestBase() { @Test fun testWriteCustomInvertedOrder() = parametrizedTest { useStreaming -> val obj = C(1, 2) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"b":2,"a":1}""", s) } @Test fun testWriteCustomOmitDefault() = parametrizedTest { useStreaming -> val obj = C(b = 2) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"b":2}""", s) } @Test fun testReadCustomInvertedOrder() = parametrizedTest { useStreaming -> val obj = C(1, 2) - val s = default.decodeFromString("""{"b":2,"a":1}""", useStreaming) + val s = jsonNoAltNames.decodeFromString("""{"b":2,"a":1}""", useStreaming) assertEquals(obj, s) } @Test fun testReadCustomOmitDefault() = parametrizedTest { useStreaming -> val obj = C(b = 2) - val s = default.decodeFromString("""{"b":2}""", useStreaming) + val s = jsonNoAltNames.decodeFromString("""{"b":2}""", useStreaming) assertEquals(obj, s) } @Test fun testWriteListOfOptional() = parametrizedTest { useStreaming -> val obj = listOf(C(a = 1), C(b = 2), C(3, 4)) - val s = default.encodeToString(ListSerializer(C), obj, useStreaming) + val s = jsonNoAltNames.encodeToString(ListSerializer(C), obj, useStreaming) assertEquals("""[{"b":42,"a":1},{"b":2},{"b":4,"a":3}]""", s) } @@ -202,21 +205,21 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadListOfOptional() = parametrizedTest { useStreaming -> val obj = listOf(C(a = 1), C(b = 2), C(3, 4)) val j = """[{"b":42,"a":1},{"b":2},{"b":4,"a":3}]""" - val s = default.decodeFromString(ListSerializer(C), j, useStreaming) + val s = jsonNoAltNames.decodeFromString(ListSerializer(C), j, useStreaming) assertEquals(obj, s) } @Test fun testWriteOptionalList1() = parametrizedTest { useStreaming -> val obj = CList1(listOf(C(a = 1), C(b = 2), C(3, 4))) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"c":[{"b":42,"a":1},{"b":2},{"b":4,"a":3}]}""", s) } @Test fun testWriteOptionalList1Quoted() = parametrizedTest { useStreaming -> val obj = CList1(listOf(C(a = 1), C(b = 2), C(3, 4))) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"c":[{"b":42,"a":1},{"b":2},{"b":4,"a":3}]}""", s) } @@ -224,13 +227,13 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList1() = parametrizedTest { useStreaming -> val obj = CList1(listOf(C(a = 1), C(b = 2), C(3, 4))) val j = """{"c":[{"b":42,"a":1},{"b":2},{"b":4,"a":3}]}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList2a() = parametrizedTest { useStreaming -> val obj = CList2(7, listOf(C(a = 5), C(b = 6), C(7, 8))) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"c":[{"b":42,"a":5},{"b":6},{"b":8,"a":7}],"d":7}""", s) } @@ -238,13 +241,13 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList2a() = parametrizedTest { useStreaming -> val obj = CList2(7, listOf(C(a = 5), C(b = 6), C(7, 8))) val j = """{"c":[{"b":42,"a":5},{"b":6},{"b":8,"a":7}],"d":7}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList2b() = parametrizedTest { useStreaming -> val obj = CList2(c = listOf(C(a = 5), C(b = 6), C(7, 8))) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"c":[{"b":42,"a":5},{"b":6},{"b":8,"a":7}]}""", s) } @@ -252,13 +255,13 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList2b() = parametrizedTest { useStreaming -> val obj = CList2(c = listOf(C(a = 5), C(b = 6), C(7, 8))) val j = """{"c":[{"b":42,"a":5},{"b":6},{"b":8,"a":7}]}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList3a() = parametrizedTest { useStreaming -> val obj = CList3(listOf(C(a = 1), C(b = 2), C(3, 4)), 99) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"e":[{"b":42,"a":1},{"b":2},{"b":4,"a":3}],"f":99}""", s) } @@ -266,13 +269,13 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList3a() = parametrizedTest { useStreaming -> val obj = CList3(listOf(C(a = 1), C(b = 2), C(3, 4)), 99) val j = """{"e":[{"b":42,"a":1},{"b":2},{"b":4,"a":3}],"f":99}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList3b() = parametrizedTest { useStreaming -> val obj = CList3(f = 99) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"f":99}""", s) } @@ -280,13 +283,13 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList3b() = parametrizedTest { useStreaming -> val obj = CList3(f = 99) val j = """{"f":99}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList4a() = parametrizedTest { useStreaming -> val obj = CList4(listOf(C(a = 1), C(b = 2), C(3, 4)), 54) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"h":54,"g":[{"b":42,"a":1},{"b":2},{"b":4,"a":3}]}""", s) } @@ -294,14 +297,14 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList4a() = parametrizedTest { useStreaming -> val obj = CList4(listOf(C(a = 1), C(b = 2), C(3, 4)), 54) val j = """{"h":54,"g":[{"b":42,"a":1},{"b":2},{"b":4,"a":3}]}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList4b() = parametrizedTest { useStreaming -> val obj = CList4(h = 97) val j = """{"h":97}""" - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals(j, s) } @@ -309,13 +312,13 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList4b() = parametrizedTest { useStreaming -> val obj = CList4(h = 97) val j = """{"h":97}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList5a() = parametrizedTest { useStreaming -> val obj = CList5(listOf(9, 8, 7, 6, 5), 5) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"h":5,"g":[9,8,7,6,5]}""", s) } @@ -323,13 +326,13 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList5a() = parametrizedTest { useStreaming -> val obj = CList5(listOf(9, 8, 7, 6, 5), 5) val j = """{"h":5,"g":[9,8,7,6,5]}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testWriteOptionalList5b() = parametrizedTest { useStreaming -> val obj = CList5(h = 999) - val s = default.encodeToString(obj, useStreaming) + val s = jsonNoAltNames.encodeToString(obj, useStreaming) assertEquals("""{"h":999}""", s) } @@ -337,14 +340,14 @@ class JsonCustomSerializersTest : JsonTestBase() { fun testReadOptionalList5b() = parametrizedTest { useStreaming -> val obj = CList5(h = 999) val j = """{"h":999}""" - assertEquals(obj, default.decodeFromString(j, useStreaming)) + assertEquals(obj, jsonNoAltNames.decodeFromString(j, useStreaming)) } @Test fun testMapBuiltinsTest() = parametrizedTest { useStreaming -> val map = mapOf(1 to "1", 2 to "2") val serial = MapSerializer(Int.serializer(), String.serializer()) - val s = default.encodeToString(serial, map, useStreaming) + val s = jsonNoAltNames.encodeToString(serial, map, useStreaming) assertEquals("""{"1":"1","2":"2"}""", s) } diff --git a/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt b/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt index 21a2169d15..d0ab7821c0 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt @@ -97,8 +97,3 @@ abstract class JsonTestBase { } } } - -inline fun assertFailsWithMessage(message: String, block: () -> Unit) { - val exception = assertFailsWith(T::class, null, block) - assertTrue(exception.message!!.contains(message), "Expected message '${exception.message}' to contain substring '$message'") -} diff --git a/formats/json/commonTest/src/kotlinx/serialization/test/TestingFramework.kt b/formats/json/commonTest/src/kotlinx/serialization/test/TestingFramework.kt index b7bbd56e3c..4f35051531 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/test/TestingFramework.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/test/TestingFramework.kt @@ -1,12 +1,12 @@ /* - * Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.serialization.test import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlin.test.assertEquals +import kotlin.test.* inline fun assertStringFormAndRestored( @@ -51,3 +51,15 @@ inline fun assertSerializedAndRestored( if (printResult) println("[Restored form] $restored") assertEquals(original, restored) } + +inline fun assertFailsWithMessage( + message: String, + assertionMessage: String? = null, + block: () -> Unit +) { + val exception = assertFailsWith(T::class, assertionMessage, block) + assertTrue( + exception.message!!.contains(message), + "Expected message '${exception.message}' to contain substring '$message'" + ) +} diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/createMapForCache.kt b/formats/json/jsMain/src/kotlinx/serialization/json/internal/createMapForCache.kt new file mode 100644 index 0000000000..b51ff401b8 --- /dev/null +++ b/formats/json/jsMain/src/kotlinx/serialization/json/internal/createMapForCache.kt @@ -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.json.internal + +/** + * Creates a ConcurrentHashMap on JVM and regular HashMap on other platforms. + * To make actual use of cache in Kotlin/Native, mark a top-level object with this map + * as a @[ThreadLocal]. + */ +internal actual fun createMapForCache(initialCapacity: Int): MutableMap = HashMap(initialCapacity) diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/createMapForCache.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/createMapForCache.kt new file mode 100644 index 0000000000..cb2498458c --- /dev/null +++ b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/createMapForCache.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.json.internal + +import java.util.concurrent.* + +/** + * Creates a ConcurrentHashMap on JVM and regular HashMap on other platforms. + * To make actual use of cache in Kotlin/Native, mark a top-level object with this map + * as a @[ThreadLocal]. + */ +internal actual fun createMapForCache(initialCapacity: Int): MutableMap = + ConcurrentHashMap(initialCapacity) diff --git a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt index 9405b27a11..f5421be046 100644 --- a/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt +++ b/formats/json/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.json.* import kotlinx.serialization.modules.* +import kotlinx.serialization.test.* import org.junit.Test import java.lang.reflect.* import kotlin.reflect.* diff --git a/formats/json/nativeMain/src/kotlinx/serialization/json/internal/createMapForCache.kt b/formats/json/nativeMain/src/kotlinx/serialization/json/internal/createMapForCache.kt new file mode 100644 index 0000000000..b51ff401b8 --- /dev/null +++ b/formats/json/nativeMain/src/kotlinx/serialization/json/internal/createMapForCache.kt @@ -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.json.internal + +/** + * Creates a ConcurrentHashMap on JVM and regular HashMap on other platforms. + * To make actual use of cache in Kotlin/Native, mark a top-level object with this map + * as a @[ThreadLocal]. + */ +internal actual fun createMapForCache(initialCapacity: Int): MutableMap = HashMap(initialCapacity) diff --git a/guide/example/example-json-04.kt b/guide/example/example-json-04.kt index 37a869a223..7317cc118b 100644 --- a/guide/example/example-json-04.kt +++ b/guide/example/example-json-04.kt @@ -4,14 +4,12 @@ package example.exampleJson04 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { coerceInputValues = true } - -@Serializable -data class Project(val name: String, val language: String = "Kotlin") +@Serializable +data class Project(@JsonNames(["title"]) val name: String) fun main() { - val data = format.decodeFromString(""" - {"name":"kotlinx.serialization","language":null} - """) - println(data) + val project = Json.decodeFromString("""{"name":"kotlinx.serialization"}""") + println(project) + val oldProject = Json.decodeFromString("""{"title":"kotlinx.coroutines"}""") + println(oldProject) } diff --git a/guide/example/example-json-05.kt b/guide/example/example-json-05.kt index 16051d53b7..ec629cd934 100644 --- a/guide/example/example-json-05.kt +++ b/guide/example/example-json-05.kt @@ -4,16 +4,14 @@ package example.exampleJson05 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { encodeDefaults = true } +val format = Json { coerceInputValues = true } @Serializable -class Project( - val name: String, - val language: String = "Kotlin", - val website: String? = null -) +data class Project(val name: String, val language: String = "Kotlin") fun main() { - val data = Project("kotlinx.serialization") - println(format.encodeToString(data)) + val data = format.decodeFromString(""" + {"name":"kotlinx.serialization","language":null} + """) + println(data) } diff --git a/guide/example/example-json-06.kt b/guide/example/example-json-06.kt index af28353374..17341c0eae 100644 --- a/guide/example/example-json-06.kt +++ b/guide/example/example-json-06.kt @@ -4,15 +4,16 @@ package example.exampleJson06 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { allowStructuredMapKeys = true } +val format = Json { encodeDefaults = true } @Serializable -data class Project(val name: String) - -fun main() { - val map = mapOf( - Project("kotlinx.serialization") to "Serialization", - Project("kotlinx.coroutines") to "Coroutines" - ) - println(format.encodeToString(map)) +class Project( + val name: String, + val language: String = "Kotlin", + val website: String? = null +) + +fun main() { + val data = Project("kotlinx.serialization") + println(format.encodeToString(data)) } diff --git a/guide/example/example-json-07.kt b/guide/example/example-json-07.kt index 013558fc23..94e6016f1f 100644 --- a/guide/example/example-json-07.kt +++ b/guide/example/example-json-07.kt @@ -4,14 +4,15 @@ package example.exampleJson07 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { allowSpecialFloatingPointValues = true } +val format = Json { allowStructuredMapKeys = true } -@Serializable -class Data( - val value: Double -) - -fun main() { - val data = Data(Double.NaN) - println(format.encodeToString(data)) +@Serializable +data class Project(val name: String) + +fun main() { + val map = mapOf( + Project("kotlinx.serialization") to "Serialization", + Project("kotlinx.coroutines") to "Coroutines" + ) + println(format.encodeToString(map)) } diff --git a/guide/example/example-json-08.kt b/guide/example/example-json-08.kt index e4e4357976..a20dc4b3d4 100644 --- a/guide/example/example-json-08.kt +++ b/guide/example/example-json-08.kt @@ -4,18 +4,14 @@ package example.exampleJson08 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { classDiscriminator = "#class" } +val format = Json { allowSpecialFloatingPointValues = true } @Serializable -sealed class Project { - abstract val name: String -} - -@Serializable -@SerialName("owned") -class OwnedProject(override val name: String, val owner: String) : Project() +class Data( + val value: Double +) fun main() { - val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") + val data = Data(Double.NaN) println(format.encodeToString(data)) -} +} diff --git a/guide/example/example-json-09.kt b/guide/example/example-json-09.kt index e3eb74c907..fe90e8a612 100644 --- a/guide/example/example-json-09.kt +++ b/guide/example/example-json-09.kt @@ -4,9 +4,18 @@ package example.exampleJson09 import kotlinx.serialization.* import kotlinx.serialization.json.* -fun main() { - val element = Json.parseToJsonElement(""" - {"name":"kotlinx.serialization","language":"Kotlin"} - """) - println(element) +val format = Json { classDiscriminator = "#class" } + +@Serializable +sealed class Project { + abstract val name: String } + +@Serializable +@SerialName("owned") +class OwnedProject(override val name: String, val owner: String) : Project() + +fun main() { + val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") + println(format.encodeToString(data)) +} diff --git a/guide/example/example-json-10.kt b/guide/example/example-json-10.kt index 7a3c221ee9..6a1b5d9b11 100644 --- a/guide/example/example-json-10.kt +++ b/guide/example/example-json-10.kt @@ -6,13 +6,7 @@ import kotlinx.serialization.json.* fun main() { val element = Json.parseToJsonElement(""" - { - "name": "kotlinx.serialization", - "forks": [{"votes": 42}, {"votes": 9000}, {}] - } + {"name":"kotlinx.serialization","language":"Kotlin"} """) - val sum = element - .jsonObject["forks"]!! - .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 } - println(sum) + println(element) } diff --git a/guide/example/example-json-11.kt b/guide/example/example-json-11.kt index ac0ee240ef..150cb0097a 100644 --- a/guide/example/example-json-11.kt +++ b/guide/example/example-json-11.kt @@ -5,19 +5,14 @@ import kotlinx.serialization.* import kotlinx.serialization.json.* fun main() { - val element = buildJsonObject { - put("name", "kotlinx.serialization") - putJsonObject("owner") { - put("name", "kotlin") + val element = Json.parseToJsonElement(""" + { + "name": "kotlinx.serialization", + "forks": [{"votes": 42}, {"votes": 9000}, {}] } - putJsonArray("forks") { - addJsonObject { - put("votes", 42) - } - addJsonObject { - put("votes", 9000) - } - } - } - println(element) + """) + val sum = element + .jsonObject["forks"]!! + .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 } + println(sum) } diff --git a/guide/example/example-json-12.kt b/guide/example/example-json-12.kt index 5d40dd15e4..8923b6830a 100644 --- a/guide/example/example-json-12.kt +++ b/guide/example/example-json-12.kt @@ -4,14 +4,20 @@ package example.exampleJson12 import kotlinx.serialization.* import kotlinx.serialization.json.* -@Serializable -data class Project(val name: String, val language: String) - fun main() { val element = buildJsonObject { put("name", "kotlinx.serialization") - put("language", "Kotlin") + putJsonObject("owner") { + put("name", "kotlin") + } + putJsonArray("forks") { + addJsonObject { + put("votes", 42) + } + addJsonObject { + put("votes", 9000) + } + } } - val data = Json.decodeFromJsonElement(element) - println(data) + println(element) } diff --git a/guide/example/example-json-13.kt b/guide/example/example-json-13.kt index 8443329ab0..2e15c97069 100644 --- a/guide/example/example-json-13.kt +++ b/guide/example/example-json-13.kt @@ -4,29 +4,14 @@ package example.exampleJson13 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.builtins.* - @Serializable -data class Project( - val name: String, - @Serializable(with = UserListSerializer::class) - val users: List -) - -@Serializable -data class User(val name: String) - -object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) { - // If response is not an array, then it is a single object that should be wrapped into the array - override fun transformDeserialize(element: JsonElement): JsonElement = - if (element !is JsonArray) JsonArray(listOf(element)) else element -} +data class Project(val name: String, val language: String) -fun main() { - println(Json.decodeFromString(""" - {"name":"kotlinx.serialization","users":{"name":"kotlin"}} - """)) - println(Json.decodeFromString(""" - {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]} - """)) +fun main() { + val element = buildJsonObject { + put("name", "kotlinx.serialization") + put("language", "Kotlin") + } + val data = Json.decodeFromJsonElement(element) + println(data) } diff --git a/guide/example/example-json-14.kt b/guide/example/example-json-14.kt index 2bfcf14a26..98099912b2 100644 --- a/guide/example/example-json-14.kt +++ b/guide/example/example-json-14.kt @@ -17,14 +17,16 @@ data class Project( data class User(val name: String) object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) { - - override fun transformSerialize(element: JsonElement): JsonElement { - require(element is JsonArray) // we are using this serializer with lists only - return element.singleOrNull() ?: element - } + // If response is not an array, then it is a single object that should be wrapped into the array + override fun transformDeserialize(element: JsonElement): JsonElement = + if (element !is JsonArray) JsonArray(listOf(element)) else element } fun main() { - val data = Project("kotlinx.serialization", listOf(User("kotlin"))) - println(Json.encodeToString(data)) + println(Json.decodeFromString(""" + {"name":"kotlinx.serialization","users":{"name":"kotlin"}} + """)) + println(Json.decodeFromString(""" + {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]} + """)) } diff --git a/guide/example/example-json-15.kt b/guide/example/example-json-15.kt index 717b2d6213..034cb85ab7 100644 --- a/guide/example/example-json-15.kt +++ b/guide/example/example-json-15.kt @@ -4,19 +4,27 @@ package example.exampleJson15 import kotlinx.serialization.* import kotlinx.serialization.json.* +import kotlinx.serialization.builtins.* + +@Serializable +data class Project( + val name: String, + @Serializable(with = UserListSerializer::class) + val users: List +) + @Serializable -class Project(val name: String, val language: String) +data class User(val name: String) -object ProjectSerializer : JsonTransformingSerializer(Project.serializer()) { - override fun transformSerialize(element: JsonElement): JsonElement = - // Filter out top-level key value pair with the key "language" and the value "Kotlin" - JsonObject(element.jsonObject.filterNot { - (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin" - }) -} +object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) { + + override fun transformSerialize(element: JsonElement): JsonElement { + require(element is JsonArray) // we are using this serializer with lists only + return element.singleOrNull() ?: element + } +} -fun main() { - val data = Project("kotlinx.serialization", "Kotlin") - println(Json.encodeToString(data)) // using plugin-generated serializer - println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer +fun main() { + val data = Project("kotlinx.serialization", listOf(User("kotlin"))) + println(Json.encodeToString(data)) } diff --git a/guide/example/example-json-16.kt b/guide/example/example-json-16.kt index 43ea1aa1db..1c84da4b2d 100644 --- a/guide/example/example-json-16.kt +++ b/guide/example/example-json-16.kt @@ -4,33 +4,19 @@ package example.exampleJson16 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.builtins.* - @Serializable -abstract class Project { - abstract val name: String -} - -@Serializable -data class BasicProject(override val name: String): Project() +class Project(val name: String, val language: String) - -@Serializable -data class OwnedProject(override val name: String, val owner: String) : Project() - -object ProjectSerializer : JsonContentPolymorphicSerializer(Project::class) { - override fun selectDeserializer(element: JsonElement) = when { - "owner" in element.jsonObject -> OwnedProject.serializer() - else -> BasicProject.serializer() - } -} +object ProjectSerializer : JsonTransformingSerializer(Project.serializer()) { + override fun transformSerialize(element: JsonElement): JsonElement = + // Filter out top-level key value pair with the key "language" and the value "Kotlin" + JsonObject(element.jsonObject.filterNot { + (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin" + }) +} fun main() { - val data = listOf( - OwnedProject("kotlinx.serialization", "kotlin"), - BasicProject("example") - ) - val string = Json.encodeToString(ListSerializer(ProjectSerializer), data) - println(string) - println(Json.decodeFromString(ListSerializer(ProjectSerializer), string)) + val data = Project("kotlinx.serialization", "Kotlin") + println(Json.encodeToString(data)) // using plugin-generated serializer + println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer } diff --git a/guide/example/example-json-17.kt b/guide/example/example-json-17.kt index c5ce88ca37..c217a12dab 100644 --- a/guide/example/example-json-17.kt +++ b/guide/example/example-json-17.kt @@ -4,56 +4,33 @@ package example.exampleJson17 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* +import kotlinx.serialization.builtins.* -@Serializable(with = ResponseSerializer::class) -sealed class Response { - data class Ok(val data: T) : Response() - data class Error(val message: String) : Response() -} +@Serializable +abstract class Project { + abstract val name: String +} -class ResponseSerializer(private val dataSerializer: KSerializer) : KSerializer> { - override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) { - element("Ok", buildClassSerialDescriptor("Ok") { - element("message") - }) - element("Error", dataSerializer.descriptor) - } +@Serializable +data class BasicProject(override val name: String): Project() - override fun deserialize(decoder: Decoder): Response { - // Decoder -> JsonDecoder - require(decoder is JsonDecoder) // this class can be decoded only by Json - // JsonDecoder -> JsonElement - val element = decoder.decodeJsonElement() - // JsonElement -> value - if (element is JsonObject && "error" in element) - return Response.Error(element["error"]!!.jsonPrimitive.content) - return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element)) - } + +@Serializable +data class OwnedProject(override val name: String, val owner: String) : Project() - override fun serialize(encoder: Encoder, value: Response) { - // Encoder -> JsonEncoder - require(encoder is JsonEncoder) // This class can be encoded only by Json - // value -> JsonElement - val element = when (value) { - is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data) - is Response.Error -> buildJsonObject { put("error", value.message) } - } - // JsonElement -> JsonEncoder - encoder.encodeJsonElement(element) +object ProjectSerializer : JsonContentPolymorphicSerializer(Project::class) { + override fun selectDeserializer(element: JsonElement) = when { + "owner" in element.jsonObject -> OwnedProject.serializer() + else -> BasicProject.serializer() } } -@Serializable -data class Project(val name: String) - fun main() { - val responses = listOf( - Response.Ok(Project("kotlinx.serialization")), - Response.Error("Not found") + val data = listOf( + OwnedProject("kotlinx.serialization", "kotlin"), + BasicProject("example") ) - val string = Json.encodeToString(responses) + val string = Json.encodeToString(ListSerializer(ProjectSerializer), data) println(string) - println(Json.decodeFromString>>(string)) + println(Json.decodeFromString(ListSerializer(ProjectSerializer), string)) } diff --git a/guide/example/example-json-18.kt b/guide/example/example-json-18.kt index 325472f11d..22e082acb1 100644 --- a/guide/example/example-json-18.kt +++ b/guide/example/example-json-18.kt @@ -7,31 +7,53 @@ import kotlinx.serialization.json.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* -data class UnknownProject(val name: String, val details: JsonObject) +@Serializable(with = ResponseSerializer::class) +sealed class Response { + data class Ok(val data: T) : Response() + data class Error(val message: String) : Response() +} -object UnknownProjectSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") { - element("name") - element("details") +class ResponseSerializer(private val dataSerializer: KSerializer) : KSerializer> { + override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) { + element("Ok", buildClassSerialDescriptor("Ok") { + element("message") + }) + element("Error", dataSerializer.descriptor) } - override fun deserialize(decoder: Decoder): UnknownProject { - // Cast to JSON-specific interface - val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON") - // Read the whole content as JSON - val json = jsonInput.decodeJsonElement().jsonObject - // Extract and remove name property - val name = json.getValue("name").jsonPrimitive.content - val details = json.toMutableMap() - details.remove("name") - return UnknownProject(name, JsonObject(details)) + override fun deserialize(decoder: Decoder): Response { + // Decoder -> JsonDecoder + require(decoder is JsonDecoder) // this class can be decoded only by Json + // JsonDecoder -> JsonElement + val element = decoder.decodeJsonElement() + // JsonElement -> value + if (element is JsonObject && "error" in element) + return Response.Error(element["error"]!!.jsonPrimitive.content) + return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element)) } - override fun serialize(encoder: Encoder, value: UnknownProject) { - error("Serialization is not supported") + override fun serialize(encoder: Encoder, value: Response) { + // Encoder -> JsonEncoder + require(encoder is JsonEncoder) // This class can be encoded only by Json + // value -> JsonElement + val element = when (value) { + is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data) + is Response.Error -> buildJsonObject { put("error", value.message) } + } + // JsonElement -> JsonEncoder + encoder.encodeJsonElement(element) } } +@Serializable +data class Project(val name: String) + fun main() { - println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}""")) + val responses = listOf( + Response.Ok(Project("kotlinx.serialization")), + Response.Error("Not found") + ) + val string = Json.encodeToString(responses) + println(string) + println(Json.decodeFromString>>(string)) } diff --git a/guide/example/example-json-19.kt b/guide/example/example-json-19.kt new file mode 100644 index 0000000000..e0d99c8f99 --- /dev/null +++ b/guide/example/example-json-19.kt @@ -0,0 +1,37 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +package example.exampleJson19 + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +data class UnknownProject(val name: String, val details: JsonObject) + +object UnknownProjectSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") { + element("name") + element("details") + } + + override fun deserialize(decoder: Decoder): UnknownProject { + // Cast to JSON-specific interface + val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON") + // Read the whole content as JSON + val json = jsonInput.decodeJsonElement().jsonObject + // Extract and remove name property + val name = json.getValue("name").jsonPrimitive.content + val details = json.toMutableMap() + details.remove("name") + return UnknownProject(name, JsonObject(details)) + } + + override fun serialize(encoder: Encoder, value: UnknownProject) { + error("Serialization is not supported") + } +} + +fun main() { + println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}""")) +} diff --git a/guide/test/JsonTest.kt b/guide/test/JsonTest.kt index ca16ee76b6..7e26b8f04b 100644 --- a/guide/test/JsonTest.kt +++ b/guide/test/JsonTest.kt @@ -32,108 +32,116 @@ class JsonTest { @Test fun testExampleJson04() { captureOutput("ExampleJson04") { example.exampleJson04.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, language=Kotlin)" + "Project(name=kotlinx.serialization)", + "Project(name=kotlinx.coroutines)" ) } @Test fun testExampleJson05() { captureOutput("ExampleJson05") { example.exampleJson05.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\",\"website\":null}" + "Project(name=kotlinx.serialization, language=Kotlin)" ) } @Test fun testExampleJson06() { captureOutput("ExampleJson06") { example.exampleJson06.main() }.verifyOutputLines( - "[{\"name\":\"kotlinx.serialization\"},\"Serialization\",{\"name\":\"kotlinx.coroutines\"},\"Coroutines\"]" + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\",\"website\":null}" ) } @Test fun testExampleJson07() { captureOutput("ExampleJson07") { example.exampleJson07.main() }.verifyOutputLines( - "{\"value\":NaN}" + "[{\"name\":\"kotlinx.serialization\"},\"Serialization\",{\"name\":\"kotlinx.coroutines\"},\"Coroutines\"]" ) } @Test fun testExampleJson08() { captureOutput("ExampleJson08") { example.exampleJson08.main() }.verifyOutputLines( - "{\"#class\":\"owned\",\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}" + "{\"value\":NaN}" ) } @Test fun testExampleJson09() { captureOutput("ExampleJson09") { example.exampleJson09.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}" + "{\"#class\":\"owned\",\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}" ) } @Test fun testExampleJson10() { captureOutput("ExampleJson10") { example.exampleJson10.main() }.verifyOutputLines( - "9042" + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}" ) } @Test fun testExampleJson11() { captureOutput("ExampleJson11") { example.exampleJson11.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}" + "9042" ) } @Test fun testExampleJson12() { captureOutput("ExampleJson12") { example.exampleJson12.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, language=Kotlin)" + "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}" ) } @Test fun testExampleJson13() { captureOutput("ExampleJson13") { example.exampleJson13.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, users=[User(name=kotlin)])", - "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])" + "Project(name=kotlinx.serialization, language=Kotlin)" ) } @Test fun testExampleJson14() { captureOutput("ExampleJson14") { example.exampleJson14.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" + "Project(name=kotlinx.serialization, users=[User(name=kotlin)])", + "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])" ) } @Test fun testExampleJson15() { captureOutput("ExampleJson15") { example.exampleJson15.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", - "{\"name\":\"kotlinx.serialization\"}" + "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" ) } @Test fun testExampleJson16() { captureOutput("ExampleJson16") { example.exampleJson16.main() }.verifyOutputLines( - "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]", - "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]" + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", + "{\"name\":\"kotlinx.serialization\"}" ) } @Test fun testExampleJson17() { captureOutput("ExampleJson17") { example.exampleJson17.main() }.verifyOutputLines( - "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]", - "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]" + "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]", + "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]" ) } @Test fun testExampleJson18() { captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines( + "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]", + "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]" + ) + } + + @Test + fun testExampleJson19() { + captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines( "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})" ) }