1 /*
<lambda>null2 * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4 @file:OptIn(ExperimentalSerializationApi::class)
5
6 package kotlinx.serialization.json.internal
7
8 import kotlinx.serialization.*
9 import kotlinx.serialization.descriptors.*
10 import kotlinx.serialization.encoding.*
11 import kotlinx.serialization.json.*
12 import kotlin.native.concurrent.*
13
14 internal val JsonDeserializationNamesKey = DescriptorSchemaCache.Key<Map<String, Int>>()
15
16 internal val JsonSerializationNamesKey = DescriptorSchemaCache.Key<Array<String>>()
17
18 private fun SerialDescriptor.buildDeserializationNamesMap(json: Json): Map<String, Int> {
19 fun MutableMap<String, Int>.putOrThrow(name: String, index: Int) {
20 val entity = if (kind == SerialKind.ENUM) "enum value" else "property"
21 if (name in this) {
22 throw JsonException(
23 "The suggested name '$name' for $entity ${getElementName(index)} is already one of the names for $entity " +
24 "${getElementName(getValue(name))} in ${this@buildDeserializationNamesMap}"
25 )
26 }
27 this[name] = index
28 }
29
30 val builder: MutableMap<String, Int> =
31 mutableMapOf() // can be not concurrent because it is only read after creation and safely published to concurrent map
32 val useLowercaseEnums = json.decodeCaseInsensitive(this)
33 val strategyForClasses = namingStrategy(json)
34 for (i in 0 until elementsCount) {
35 getElementAnnotations(i).filterIsInstance<JsonNames>().singleOrNull()?.names?.forEach { name ->
36 builder.putOrThrow(if (useLowercaseEnums) name.lowercase() else name, i)
37 }
38 val nameToPut = when {
39 // the branches do not intersect because useLowercase = true for enums only, and strategy != null for classes only.
40 useLowercaseEnums -> getElementName(i).lowercase()
41 strategyForClasses != null -> strategyForClasses.serialNameForJson(this, i, getElementName(i))
42 else -> null
43 }
44 nameToPut?.let { builder.putOrThrow(it, i) }
45 }
46 return builder.ifEmpty { emptyMap() }
47 }
48
49 /**
50 * Contains strategy-mapped names and @JsonNames,
51 * so original names are not stored when strategy is `null`.
52 */
deserializationNamesMapnull53 internal fun Json.deserializationNamesMap(descriptor: SerialDescriptor): Map<String, Int> =
54 schemaCache.getOrPut(descriptor, JsonDeserializationNamesKey) { descriptor.buildDeserializationNamesMap(this) }
55
serializationNamesIndicesnull56 internal fun SerialDescriptor.serializationNamesIndices(json: Json, strategy: JsonNamingStrategy): Array<String> =
57 json.schemaCache.getOrPut(this, JsonSerializationNamesKey) {
58 Array(elementsCount) { i ->
59 val baseName = getElementName(i)
60 strategy.serialNameForJson(this, i, baseName)
61 }
62 }
63
getJsonElementNamenull64 internal fun SerialDescriptor.getJsonElementName(json: Json, index: Int): String {
65 val strategy = namingStrategy(json)
66 return if (strategy == null) getElementName(index) else serializationNamesIndices(json, strategy)[index]
67 }
68
namingStrategynull69 internal fun SerialDescriptor.namingStrategy(json: Json) =
70 if (kind == StructureKind.CLASS) json.configuration.namingStrategy else null
71
72 private fun SerialDescriptor.getJsonNameIndexSlowPath(json: Json, name: String): Int =
73 json.deserializationNamesMap(this)[name] ?: CompositeDecoder.UNKNOWN_NAME
74
75 private fun Json.decodeCaseInsensitive(descriptor: SerialDescriptor) =
76 configuration.decodeEnumsCaseInsensitive && descriptor.kind == SerialKind.ENUM
77
78 /**
79 * Serves same purpose as [SerialDescriptor.getElementIndex] but respects [JsonNames] annotation
80 * and [JsonConfiguration] settings.
81 */
82 @OptIn(ExperimentalSerializationApi::class)
83 internal fun SerialDescriptor.getJsonNameIndex(json: Json, name: String): Int {
84 if (json.decodeCaseInsensitive(this)) {
85 return getJsonNameIndexSlowPath(json, name.lowercase())
86 }
87
88 val strategy = namingStrategy(json)
89 if (strategy != null) return getJsonNameIndexSlowPath(json, name)
90 val index = getElementIndex(name)
91 // Fast path, do not go through ConcurrentHashMap.get
92 // Note, it blocks ability to detect collisions between the primary name and alternate,
93 // but it eliminates a significant performance penalty (about -15% without this optimization)
94 if (index != CompositeDecoder.UNKNOWN_NAME) return index
95 if (!json.configuration.useAlternativeNames) return index
96 // Slow path
97 return getJsonNameIndexSlowPath(json, name)
98 }
99
100 /**
101 * Throws on [CompositeDecoder.UNKNOWN_NAME]
102 */
103 @OptIn(ExperimentalSerializationApi::class)
getJsonNameIndexOrThrownull104 internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String, suffix: String = ""): Int {
105 val index = getJsonNameIndex(json, name)
106 if (index == CompositeDecoder.UNKNOWN_NAME)
107 throw SerializationException("$serialName does not contain element with name '$name'$suffix")
108 return index
109 }
110
111 @OptIn(ExperimentalSerializationApi::class)
tryCoerceValuenull112 internal inline fun Json.tryCoerceValue(
113 descriptor: SerialDescriptor,
114 index: Int,
115 peekNull: (consume: Boolean) -> Boolean,
116 peekString: () -> String?,
117 onEnumCoercing: () -> Unit = {}
118 ): Boolean {
119 if (!descriptor.isElementOptional(index)) return false
120 val elementDescriptor = descriptor.getElementDescriptor(index)
121 if (!elementDescriptor.isNullable && peekNull(true)) return true
122 if (elementDescriptor.kind == SerialKind.ENUM) {
123 if (elementDescriptor.isNullable && peekNull(false)) {
124 return false
125 }
126
127 val enumValue = peekString()
128 ?: return false // if value is not a string, decodeEnum() will throw correct exception
129 val enumIndex = elementDescriptor.getJsonNameIndex(this, enumValue)
130 if (enumIndex == CompositeDecoder.UNKNOWN_NAME) {
131 onEnumCoercing()
132 return true
133 }
134 }
135 return false
136 }
137