diff options
Diffstat (limited to 'formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt')
-rw-r--r-- | formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt | 89 |
1 files changed, 70 insertions, 19 deletions
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt index 93e604a7..9128f3a2 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt @@ -1,6 +1,7 @@ /* * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalSerializationApi::class) package kotlinx.serialization.json.internal @@ -10,37 +11,82 @@ import kotlinx.serialization.encoding.* import kotlinx.serialization.json.* import kotlin.native.concurrent.* -@SharedImmutable -internal val JsonAlternativeNamesKey = DescriptorSchemaCache.Key<Map<String, Int>>() +internal val JsonDeserializationNamesKey = DescriptorSchemaCache.Key<Map<String, Int>>() -@OptIn(ExperimentalSerializationApi::class) -internal fun SerialDescriptor.buildAlternativeNamesMap(): Map<String, Int> { +internal val JsonSerializationNamesKey = DescriptorSchemaCache.Key<Array<String>>() + +private fun SerialDescriptor.buildDeserializationNamesMap(json: Json): Map<String, Int> { fun MutableMap<String, Int>.putOrThrow(name: String, index: Int) { + val entity = if (kind == SerialKind.ENUM) "enum value" else "property" 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}" + "The suggested name '$name' for $entity ${getElementName(index)} is already one of the names for $entity " + + "${getElementName(getValue(name))} in ${this@buildDeserializationNamesMap}" ) } this[name] = index } - var builder: MutableMap<String, Int>? = null + val builder: MutableMap<String, Int> = + mutableMapOf() // can be not concurrent because it is only read after creation and safely published to concurrent map + val useLowercaseEnums = json.decodeCaseInsensitive(this) + val strategyForClasses = namingStrategy(json) for (i in 0 until elementsCount) { getElementAnnotations(i).filterIsInstance<JsonNames>().singleOrNull()?.names?.forEach { name -> - if (builder == null) builder = createMapForCache(elementsCount) - builder!!.putOrThrow(name, i) + builder.putOrThrow(if (useLowercaseEnums) name.lowercase() else name, i) + } + val nameToPut = when { + // the branches do not intersect because useLowercase = true for enums only, and strategy != null for classes only. + useLowercaseEnums -> getElementName(i).lowercase() + strategyForClasses != null -> strategyForClasses.serialNameForJson(this, i, getElementName(i)) + else -> null + } + nameToPut?.let { builder.putOrThrow(it, i) } + } + return builder.ifEmpty { emptyMap() } +} + +/** + * Contains strategy-mapped names and @JsonNames, + * so original names are not stored when strategy is `null`. + */ +internal fun Json.deserializationNamesMap(descriptor: SerialDescriptor): Map<String, Int> = + schemaCache.getOrPut(descriptor, JsonDeserializationNamesKey) { descriptor.buildDeserializationNamesMap(this) } + +internal fun SerialDescriptor.serializationNamesIndices(json: Json, strategy: JsonNamingStrategy): Array<String> = + json.schemaCache.getOrPut(this, JsonSerializationNamesKey) { + Array(elementsCount) { i -> + val baseName = getElementName(i) + strategy.serialNameForJson(this, i, baseName) } } - return builder ?: emptyMap() + +internal fun SerialDescriptor.getJsonElementName(json: Json, index: Int): String { + val strategy = namingStrategy(json) + return if (strategy == null) getElementName(index) else serializationNamesIndices(json, strategy)[index] } +internal fun SerialDescriptor.namingStrategy(json: Json) = + if (kind == StructureKind.CLASS) json.configuration.namingStrategy else null + +private fun SerialDescriptor.getJsonNameIndexSlowPath(json: Json, name: String): Int = + json.deserializationNamesMap(this)[name] ?: CompositeDecoder.UNKNOWN_NAME + +private fun Json.decodeCaseInsensitive(descriptor: SerialDescriptor) = + configuration.decodeEnumsCaseInsensitive && descriptor.kind == SerialKind.ENUM + /** - * Serves same purpose as [SerialDescriptor.getElementIndex] but respects - * [JsonNames] annotation and [JsonConfiguration.useAlternativeNames] state. + * Serves same purpose as [SerialDescriptor.getElementIndex] but respects [JsonNames] annotation + * and [JsonConfiguration] settings. */ @OptIn(ExperimentalSerializationApi::class) internal fun SerialDescriptor.getJsonNameIndex(json: Json, name: String): Int { + if (json.decodeCaseInsensitive(this)) { + return getJsonNameIndexSlowPath(json, name.lowercase()) + } + + val strategy = namingStrategy(json) + if (strategy != null) return getJsonNameIndexSlowPath(json, name) val index = getElementIndex(name) // Fast path, do not go through ConcurrentHashMap.get // Note, it blocks ability to detect collisions between the primary name and alternate, @@ -48,9 +94,7 @@ internal fun SerialDescriptor.getJsonNameIndex(json: Json, name: String): Int { if (index != CompositeDecoder.UNKNOWN_NAME) return index if (!json.configuration.useAlternativeNames) return index // Slow path - val alternativeNamesMap = - json.schemaCache.getOrPut(this, JsonAlternativeNamesKey, this::buildAlternativeNamesMap) - return alternativeNamesMap[name] ?: CompositeDecoder.UNKNOWN_NAME + return getJsonNameIndexSlowPath(json, name) } /** @@ -66,15 +110,22 @@ internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String, @OptIn(ExperimentalSerializationApi::class) internal inline fun Json.tryCoerceValue( - elementDescriptor: SerialDescriptor, - peekNull: () -> Boolean, + descriptor: SerialDescriptor, + index: Int, + peekNull: (consume: Boolean) -> Boolean, peekString: () -> String?, onEnumCoercing: () -> Unit = {} ): Boolean { - if (!elementDescriptor.isNullable && peekNull()) return true + if (!descriptor.isElementOptional(index)) return false + val elementDescriptor = descriptor.getElementDescriptor(index) + if (!elementDescriptor.isNullable && peekNull(true)) return true if (elementDescriptor.kind == SerialKind.ENUM) { + if (elementDescriptor.isNullable && peekNull(false)) { + return false + } + val enumValue = peekString() - ?: return false // if value is not a string, decodeEnum() will throw correct exception + ?: return false // if value is not a string, decodeEnum() will throw correct exception val enumIndex = elementDescriptor.getJsonNameIndex(this, enumValue) if (enumIndex == CompositeDecoder.UNKNOWN_NAME) { onEnumCoercing() |