Skip to content

Instantly share code, notes, and snippets.

@emrul
Last active February 8, 2018 19:08
Show Gist options
  • Save emrul/2d446eff2084313fcb332f07e62b2ae8 to your computer and use it in GitHub Desktop.
Save emrul/2d446eff2084313fcb332f07e62b2ae8 to your computer and use it in GitHub Desktop.
Hack to get Jsoniter to recognise JSONProperty annotations on Kotlin data classes
data class User(@JsonProperty("userName") val name: String)
val userObj = User("John")
JsoniterKotlinSupport.enable()
JsoniterAnnotationSupport.enable()
val jsonUserString = JsonStream.serialize(userObj)
// jsonUserString will contain: {"userName":"John"}
import com.jsoniter.annotation.JsonProperty
import com.jsoniter.spi.*
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.kotlinFunction
private val metadataFqName = "kotlin.Metadata"
fun Class<*>.isKotlinClass(): Boolean {
return this.declaredAnnotations.singleOrNull { it.annotationClass.java.name == metadataFqName } != null
}
class JsoniterKotlinSupport : EmptyExtension() {
companion object {
private var enabled = false
fun enable() {
if (enabled) {
return
}
enabled = true
JsoniterSpi.registerExtension(JsoniterKotlinSupport())
}
}
override fun updateClassDescriptor(desc: ClassDescriptor?) {
if ( desc?.clazz?.isKotlinClass() != true ) return
detectKotlinDataClass(desc)
}
fun detectKotlinDataClass(desc: ClassDescriptor) {
// Only interested in data classes
if ( !desc.clazz.kotlin.isData ) return
// Clear getters in the descriptor since we will access via field
desc.getters?.clear()
// Look through each declared constructor
desc.clazz.declaredConstructors.forEach { constructor ->
// See if there is a parameter with a JSONProperty annotation
val hasJsonAnnotations = constructor.kotlinFunction?.parameters?.any { parameter -> (parameter.findAnnotation<JsonProperty>() != null) } ?: false
if ( hasJsonAnnotations ) {
// Populate the constructor
desc.ctor = getCtor(desc.clazz)
desc.ctor.ctor = constructor
desc.ctor.staticMethodName = null
desc.ctor.staticFactory = null
// Go through each constructor parameter and see if it has a JSONProperty annotation
constructor.kotlinFunction?.parameters?.forEachIndexed { i, parameter ->
val jsonProperty = getJsonProperty(parameter.annotations.toTypedArray()) // parameter.findAnnotation<JsonProperty>()
val field = desc.fields.find { field -> field.name == parameter.name }
// If we have a field with the same name as the parameter then set or add the JSONProperty annotation to it
if ( field != null && jsonProperty != null ) {
if ( field.annotations == null ) {
field.annotations = arrayOf(jsonProperty)
}
else {
field.annotations = field.annotations + jsonProperty
}
}
// Deserialisation support
val binding = Binding(desc.clazz, desc.lookup, constructor.genericParameterTypes[i])
if (jsonProperty != null) {
updateBindingWithJsonProperty(binding, jsonProperty)
}
if (binding.name == null || binding.name.isEmpty()) {
binding.name = parameter.name
}
binding.annotations = parameter.annotations.toTypedArray()
desc.ctor.parameters.add(binding)
}
}
}
}
private fun getCtor(clazz: Class<*>): ConstructorDescriptor {
val cctor = ConstructorDescriptor()
try {
cctor.ctor = clazz.getDeclaredConstructor()
} catch (e: Exception) {
cctor.ctor = null
}
return cctor
}
private fun getJsonProperty(annotations: Array<Annotation>): JsonProperty? {
return getAnnotation(annotations, JsonProperty::class.java)
}
private fun <T : Annotation> getAnnotation(annotations: Array<Annotation>?, annotationClass: Class<T>): T? {
if (annotations == null) {
return null
}
return annotations
.firstOrNull { annotationClass.isAssignableFrom(it.javaClass) }
?.let {
@Suppress("UNCHECKED_CAST")
it as T
}
}
@Throws(Exception::class)
private fun reflectCall(obj: Any, methodName: String, vararg args: Any): Any {
val method = obj.javaClass.getMethod(methodName)
return method.invoke(obj, *args)
}
private fun updateBindingWithJsonProperty(binding: Binding, jsonProperty: JsonProperty) {
binding.asMissingWhenNotPresent = jsonProperty.required
binding.isNullable = jsonProperty.nullable
binding.isCollectionValueNullable = jsonProperty.collectionValueNullable
binding.shouldOmitNull = jsonProperty.omitNull
val altName = jsonProperty.value
if (!altName.isEmpty()) {
binding.name = altName
binding.fromNames = arrayOf(altName)
}
if (jsonProperty.from.isNotEmpty()) {
binding.fromNames = jsonProperty.from
}
if (jsonProperty.to.isNotEmpty()) {
binding.toNames = jsonProperty.to
}
val decoder = jsonProperty.decoder
if (decoder.java != Decoder::class.java) {
try {
binding.decoder = decoder.java.newInstance()
} catch (e: Exception) {
throw JsonException(e)
}
}
val encoder = jsonProperty.encoder
if (encoder.java != Encoder::class.java) {
try {
binding.encoder = encoder.java.newInstance()
} catch (e: Exception) {
throw JsonException(e)
}
}
val implementation = jsonProperty.implementation
if (implementation.java != Any::class.java) {
binding.valueType = ParameterizedTypeImpl.useImpl(binding.valueType, jsonProperty.implementation::class.java)
binding.valueTypeLiteral = TypeLiteral.create(binding.valueType)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment