Skip to content

Commit

Permalink
Add @JsonNames annotation
Browse files Browse the repository at this point in the history
via DescriptorSchemaCache internal mechanism
with guide, docs, and samples.

Stabilize .toString() for SerialClassDescImpl; because HashMaps on different platforms have different iteration order.

Disable primary collision detection for optimization purposes.

Add benchmarks on skipping unknown fields.
  • Loading branch information
sandwwraith committed Apr 14, 2021
1 parent 83d0faa commit 6087755
Show file tree
Hide file tree
Showing 37 changed files with 651 additions and 276 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)

}
36 changes: 36 additions & 0 deletions benchmark/src/jmh/kotlin/kotlinx/benchmarks/model/MacroTwitter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ data class MacroTwitterFeed(
val search_metadata: SearchMetadata
)

@Serializable
data class MicroTwitterFeed(
val statuses: List<TwitterReducedStatus>
)

@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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
67 changes: 52 additions & 15 deletions docs/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -151,6 +152,40 @@ Project(name=kotlinx.serialization)

<!--- TEST -->

### 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<Project>("""{"name":"kotlinx.serialization"}""")
println(project)
val oldProject = Json.decodeFromString<Project>("""{"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.

<!--- TEST -->

### Coercing input values

JSON formats that are encountered in the wild can be flexible in terms of types and evolve quickly.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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].

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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"})
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/serialization-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Once the project is set up, we can start serializing some classes.
* <a name='pretty-printing'></a>[Pretty printing](json.md#pretty-printing)
* <a name='lenient-parsing'></a>[Lenient parsing](json.md#lenient-parsing)
* <a name='ignoring-unknown-keys'></a>[Ignoring unknown keys](json.md#ignoring-unknown-keys)
* <a name='alternative-json-names'></a>[Alternative Json names](json.md#alternative-json-names)
* <a name='coercing-input-values'></a>[Coercing input values](json.md#coercing-input-values)
* <a name='encoding-defaults'></a>[Encoding defaults](json.md#encoding-defaults)
* <a name='allowing-structured-map-keys'></a>[Allowing structured map keys](json.md#allowing-structured-map-keys)
Expand Down
11 changes: 11 additions & 0 deletions formats/json/api/kotlinx-serialization-json.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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 <init> ([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;
Expand Down
15 changes: 14 additions & 1 deletion formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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())

/**
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
)
}
}
Expand Down
Loading

0 comments on commit 6087755

Please sign in to comment.