net.pwall.json.JSONConfig.kt Maven / Gradle / Ivy
/*
* @(#) JSONConfig.kt
*
* json-kotlin Kotlin JSON Auto Serialize/deserialize
* Copyright (c) 2019, 2020, 2021 Peter Wall
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.pwall.json
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.isSuperclassOf
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.isSupertypeOf
import net.pwall.json.JSONKotlinException.Companion.fail
import net.pwall.json.annotation.JSONAllowExtra
import net.pwall.json.annotation.JSONIgnore
import net.pwall.json.annotation.JSONIncludeAllProperties
import net.pwall.json.annotation.JSONIncludeIfNull
import net.pwall.json.annotation.JSONName
/**
* Configuration class for JSON Auto serialize / deserialize for Kotlin.
*
* @author Peter Wall
*/
class JSONConfig {
/** Name of property to store sealed class subclass name as discriminator */
var sealedClassDiscriminator = defaultSealedClassDiscriminator
set(newValue) {
if (newValue.isNotEmpty()) field = newValue else fail("Sealed class discriminator invalid")
}
/** Read buffer size (for `json-ktor`), arbitrarily limited to multiple of 16, not greater than 256K */
var readBufferSize = defaultBufferSize
set (newValue) {
if ((newValue and 15) == 0 && newValue in 16..(256 * 1024))
field = newValue
else
fail("Read buffer size invalid - $newValue")
}
/** Initial allocation size for stringify operations, arbitrarily limited to 16 to 256K */
var stringifyInitialSize = defaultStringifyInitialSize
set (newValue) {
if (newValue in 16..(256 * 1024))
field = newValue
else
fail("Stringify initial allocation size invalid - $newValue")
}
/** Character set (for `json-ktor` and `json-ktor-client`) */
var charset = defaultCharset
/** Switch to control how `BigInteger` is serialized / deserialized: `true` -> string, `false` -> number */
var bigIntegerString = defaultBigIntegerString
/** Switch to control how `BigDecimal` is serialized / deserialized: `true` -> string, `false` -> number */
var bigDecimalString = defaultBigDecimalString
/** Switch to control whether null fields in objects are output as "null": `true` -> yes, `false` -> no */
var includeNulls = defaultIncludeNulls
/** Switch to control whether extra fields are allowed on deserialization: `true` -> yes, `false` -> no */
var allowExtra = defaultAllowExtra
/** Switch to control whether `json-ktor` uses streamed output */
var streamOutput = defaultStreamOutput
private val fromJSONMap: MutableMap = LinkedHashMap()
private val toJSONMap: MutableMap = LinkedHashMap()
private val nameAnnotations: MutableList, KProperty.Getter>> =
arrayListOf(namePropertyPair(JSONName::class, "name"))
private val ignoreAnnotations: MutableList> = arrayListOf(JSONIgnore::class, Transient::class)
private val includeIfNullAnnotations: MutableList> = arrayListOf(JSONIncludeIfNull::class)
private val includeAllPropertiesAnnotations: MutableList> = arrayListOf(JSONIncludeAllProperties::class)
private val allowExtraPropertiesAnnotations: MutableList> = arrayListOf(JSONAllowExtra::class)
/**
* Find a `fromJSON` mapping function that will create the specified [KType], or the closest subtype of it.
*
* @param type the target type
* @return the mapping function, or `null` if not found
*/
fun findFromJSONMapping(type: KType): FromJSONMapping? {
if (type.classifier == Any::class)
return null
var best: Map.Entry? = null
for (entry in fromJSONMap.entries) {
if (entry.key.isSubtypeOf(type) && best.let { it == null || it.key.isSubtypeOf(entry.key) })
best = entry
}
return best?.value
}
/**
* Find a `fromJSON` mapping function that will create the specified [KClass], or the closest subclass of it.
*
* @param targetClass the target class
* @return the mapping function, or `null` if not found
*/
fun findFromJSONMapping(targetClass: KClass<*>): FromJSONMapping? {
if (targetClass == Any::class)
return null
var best: KClass<*>? = null
var nullable = false
var result: FromJSONMapping? = null
for (entry in fromJSONMap.entries) {
val classifier = entry.key.classifier as KClass<*>
if (classifier.isSubclassOf(targetClass) && best.let {
it == null ||
if (it == classifier) !nullable else it.isSubclassOf(classifier) }) {
best = classifier
nullable = entry.key.isMarkedNullable
result = entry.value
}
}
return result
}
/**
* Find a `toJSON` mapping function that will accept the specified [KType], or the closest supertype of it.
*
* @param type the source type
* @return the mapping function, or `null` if not found
*/
fun findToJSONMapping(type: KType): ToJSONMapping? {
var best: Map.Entry? = null
for (entry in toJSONMap.entries) {
if (entry.key.isSupertypeOf(type) && best.let { it == null || it.key.isSupertypeOf(entry.key) })
best = entry
}
return best?.value
}
/**
* Find a `toJSON` mapping function that will accept the specified [KClass], or the closest superclass of it.
*
* @param sourceClass the source class
* @return the mapping function, or `null` if not found
*/
fun findToJSONMapping(sourceClass: KClass<*>): ToJSONMapping? {
var best: KClass<*>? = null
var nullable = false
var result: ToJSONMapping? = null
for (entry in toJSONMap.entries) {
val classifier = entry.key.classifier as KClass<*>
if (classifier.isSuperclassOf(sourceClass) && best.let {
it == null ||
if (it == classifier) nullable else it.isSuperclassOf(classifier) }) {
best = classifier
nullable = entry.key.isMarkedNullable
result = entry.value
}
}
return result
}
/**
* Add custom mapping from JSON to the specified type.
*
* @param type the target type
* @param mapping the mapping function
*/
fun fromJSON(type: KType, mapping: FromJSONMapping) {
fromJSONMap[type] = mapping
}
/**
* Add custom mapping from JSON to the specified type, using a constructor that takes a single [String] parameter.
*
* @param type the target type
*/
fun fromJSONString(type: KType) {
fromJSONMap[type] = { json ->
when (json) {
null -> if (type.isMarkedNullable) null else fail("Can't deserialize null as $type")
is JSONString -> {
val resultClass = type.classifier as? KClass<*> ?: fail("Can't deserialize $type")
val constructor = resultClass.constructors.find {
it.parameters.size == 1 && it.parameters[0].type == stringType
}
constructor?.call(json.toString()) ?: fail("Can't deserialize $type")
}
else -> fail("Can't deserialize ${json::class.simpleName} as $type")
}
}
}
/**
* Add custom mapping from a specified type to JSON.
*
* @param type the source type
* @param mapping the mapping function
*/
fun toJSON(type: KType, mapping: ToJSONMapping) {
toJSONMap[type] = mapping
}
/**
* Add custom mapping from a specified type to JSON using the `toString()` function to create a JSON string.
*
* @param type the source type
*/
fun toJSONString(type: KType) {
toJSONMap[type] = { obj -> obj?.let { JSONString(it.toString())} }
}
/**
* Add custom mapping from JSON to the inferred type.
*
* @param mapping the mapping function
* @param T the type to be mapped
*/
inline fun fromJSON(noinline mapping: (JSONValue?) -> T?) {
fromJSON(JSONTypeRef.create(nullable = false).refType, mapping)
}
/**
* Add custom mapping from JSON to the inferred type, using a constructor that takes a single [String] parameter.
*
* @param T the type to be mapped
*/
inline fun fromJSONString() {
fromJSONString(JSONTypeRef.create(nullable = false).refType)
}
/**
* Add custom mapping from an inferred type to JSON.
*
* @param mapping the mapping function
* @param T the type to be mapped
*/
inline fun toJSON(noinline mapping: (T?) -> JSONValue?) {
toJSON(JSONTypeRef.create(nullable = true).refType) { mapping(it as T?) }
}
/**
* Add custom mapping from an inferred type to JSON using the `toString()` function to create a JSON string.
*
* @param T the type to be mapped
*/
inline fun toJSONString() {
toJSONString(JSONTypeRef.create(nullable = true).refType)
}
/**
* Add an annotation specification to the list of annotations that specify the name to be used when serializing or
* deserializing a property.
*
* @param nameAnnotationClass the annotation class to specify the name
* @param argumentName the name of the argument to the annotation that holds the name
* @param T the annotation class
*/
fun addNameAnnotation(nameAnnotationClass: KClass, argumentName: String) {
nameAnnotations.add(namePropertyPair(nameAnnotationClass, argumentName))
}
private fun namePropertyPair(nameAnnotationClass: KClass, argumentName: String):
Pair, KProperty.Getter> {
return nameAnnotationClass to findAnnotationStringProperty(nameAnnotationClass, argumentName).getter
}
@Suppress("UNCHECKED_CAST")
private fun findAnnotationStringProperty(annotationClass: KClass, argumentName: String):
KProperty {
for (member in annotationClass.members) {
if (member is KProperty<*> && member.name == argumentName && member.returnType == stringType) {
return member as KProperty
}
}
throw IllegalArgumentException(
"Annotation class ${annotationClass.simpleName} does not have a String property \"$argumentName\"")
}
/**
* Find the name to be used when serializing or deserializing a property, from the annotations supplied.
*
* @param annotations the [Annotation]s (from the parameter, or the property, or both)
* @return the name to be used, or `null` if no annotation found
*/
fun findNameFromAnnotation(annotations: List?): String? {
if (annotations != null) {
for (entry in nameAnnotations) {
for (annotation in annotations) {
if (annotation::class.isSubclassOf(entry.first))
return entry.second.call(annotation)
}
}
}
return null
}
/**
* Add an annotation specification to the list of annotations that specify that the property is to be ignored when
* serializing or deserializing.
*
* @param ignoreAnnotationClass the annotation class
* @param T the annotation class
*/
fun addIgnoreAnnotation(ignoreAnnotationClass: KClass) {
ignoreAnnotations.add(ignoreAnnotationClass)
}
/**
* Test whether a property has an annotation to indicate that it is to be ignored when serializing or deserializing.
*
* @param annotations the [Annotation]s (from the parameter, or the property, or both)
* @return `true` if an "ignore" annotation appears in the supplied list
*/
fun hasIgnoreAnnotation(annotations: List?) = hasBooleanAnnotation(ignoreAnnotations, annotations)
/**
* Add an annotation specification to the list of annotations that specify that the property is to be included when
* serializing or deserializing even if `null`.
*
* @param includeIfNullAnnotationClass the annotation class
* @param T the annotation class
*/
fun addIncludeIfNullAnnotation(includeIfNullAnnotationClass: KClass) {
includeIfNullAnnotations.add(includeIfNullAnnotationClass)
}
/**
* Test whether a property has an annotation to indicate that it is to be included when serializing or deserializing
* even if `null`.
*
* @param annotations the [Annotation]s (from the parameter, or the property, or both)
* @return `true` if an "include if null" annotation appears in the supplied list
*/
fun hasIncludeIfNullAnnotation(annotations: List?) =
hasBooleanAnnotation(includeIfNullAnnotations, annotations)
/**
* Add an annotation specification to the list of annotations that specify that all properties in a class are to be
* included when serializing even if `null`.
*
* @param ignoreAllPropertiesAnnotationClass the annotation class
* @param T the annotation class
*/
fun addIncludeAllPropertiesAnnotation(ignoreAllPropertiesAnnotationClass: KClass) {
includeAllPropertiesAnnotations.add(ignoreAllPropertiesAnnotationClass)
}
/**
* Test whether a property has an annotation to indicate that it is to be included when serializing even if `null`.
*
* @param annotations the [Annotation]s (from the class)
* @return `true` if an "include all properties" annotation appears in the supplied list
*/
fun hasIncludeAllPropertiesAnnotation(annotations: List?) =
hasBooleanAnnotation(includeAllPropertiesAnnotations, annotations)
/**
* Add an annotation specification to the list of annotations that specify that extra properties in a class are to
* be ignored when deserializing.
*
* @param allowExtraPropertiesAnnotationClass the annotation class
* @param T the annotation class
*/
fun addAllowExtraPropertiesAnnotation(allowExtraPropertiesAnnotationClass: KClass) {
allowExtraPropertiesAnnotations.add(allowExtraPropertiesAnnotationClass)
}
/**
* Test whether a property has an annotation to indicate that extra properties in a class are to be ignored when
* deserializing.
*
* @param annotations the [Annotation]s (from the class)
* @return `true` if an "allow extra properties" annotation appears in the supplied list
*/
fun hasAllowExtraPropertiesAnnotation(annotations: List?) =
hasBooleanAnnotation(allowExtraPropertiesAnnotations, annotations)
/**
* Test whether a property has a boolean annotation matching the specified list.
*
* @param annotationList the list of pre-configured annotation classes.
* @param annotations the [Annotation]s (from the parameter, or the property, or both)
* @return `true` if an "ignore" annotation appears in the supplied list
*/
private fun hasBooleanAnnotation(annotationList: List>, annotations: List?): Boolean {
if (annotations != null) {
for (entry in annotationList) {
for (annotation in annotations) {
if (annotation::class.isSubclassOf(entry))
return true
}
}
}
return false
}
/**
* Combine another `JSONConfig` into this one.
*
* @param config the other `JSONConfig`
*/
fun combineAll(config: JSONConfig) {
sealedClassDiscriminator = config.sealedClassDiscriminator
readBufferSize = config.readBufferSize
stringifyInitialSize = config.stringifyInitialSize
charset = config.charset
bigIntegerString = config.bigIntegerString
bigDecimalString = config.bigDecimalString
includeNulls = config.includeNulls
allowExtra = config.allowExtra
streamOutput = config.streamOutput
fromJSONMap.putAll(config.fromJSONMap)
toJSONMap.putAll(config.toJSONMap)
nameAnnotations.addAll(config.nameAnnotations)
ignoreAnnotations.addAll(config.ignoreAnnotations)
includeIfNullAnnotations.addAll(config.includeIfNullAnnotations)
includeAllPropertiesAnnotations.addAll(config.includeAllPropertiesAnnotations)
allowExtraPropertiesAnnotations.addAll(config.allowExtraPropertiesAnnotations)
}
/**
* Combine custom mappings from another `JSONConfig` into this one.
*
* @param config the other `JSONConfig`
*/
fun combineMappings(config: JSONConfig) {
fromJSONMap.putAll(config.fromJSONMap)
toJSONMap.putAll(config.toJSONMap)
}
companion object {
val stringType = String::class.createType()
const val defaultSealedClassDiscriminator = "class"
const val defaultBufferSize = DEFAULT_BUFFER_SIZE
const val defaultStringifyInitialSize = 1024
const val defaultBigIntegerString = false
const val defaultBigDecimalString = false
const val defaultIncludeNulls = false
const val defaultAllowExtra = false
const val defaultStreamOutput = false
val defaultCharset = Charsets.UTF_8
val defaultConfig = JSONConfig()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy