Skip to content

Instantly share code, notes, and snippets.

@bastman
Last active April 23, 2019 06:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bastman/85cf7d802db23d0585cc87d1ee1d88ed to your computer and use it in GitHub Desktop.
Save bastman/85cf7d802db23d0585cc87d1ee1d88ed to your computer and use it in GitHub Desktop.
kotlin jackson patchable deserializer
@RestController
class TweeterApiController() {
data class PatchTweetRequestV2(
val foo:Optional<String?>?,
val bar:Optional<String?>?,
val baz:Optional<String?>?,
//@JsonDeserialize(using = PatchableDeserializer::class)
//@get: ApiModelProperty(dataType = "[Ljava.lang.String;")
//@get: ApiModelProperty(dataType = "java.lang.String")
val message1:Patchable<String?>,
//@JsonDeserialize(using = PatchableDeserializer::class)
val message2:Patchable<String?>?,
//@JsonDeserialize(using = PatchableDeserializer::class)
val message3: Patchable<String?>?
)
@PatchMapping("/api/tweeter/{id}")
fun patchOne(
@PathVariable id: UUID,
@RequestBody req: PatchTweetRequestV2
): Any? {
println(req)
val r:Option<String?> = when(val it=req.message1) {
is Patchable.Null -> Option.Some<String?>(null)
is Patchable.Undefined -> Option.None
is Patchable.Present -> Option.Some(it.content)
}
println(r)
return req
}
}
package com.example.config
import com.example.Patchable
import com.example.PatchableDeserializer
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class Jackson {
@Bean
fun objectMapper(): ObjectMapper = defaultMapper()
companion object {
fun defaultMapper(): ObjectMapper = jacksonObjectMapper()
.registerModule(
SimpleModule()
.addDeserializer(Patchable::class.java, PatchableDeserializer())
)
//.registerModule(Jdk8Module())
//.registerModule(JavaTimeModule())
.findAndRegisterModules()
// toJson()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
// fromJson()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
.disable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
.disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
.enable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
.also {
println("==> JACKON MODULES: ${it.registeredModuleIds}")
}
}
}
// https://stackoverflow.com/questions/36159677/how-to-create-a-custom-deserializer-in-jackson-for-a-generic-type
// see: https://stackoverflow.com/questions/55166379/deserialize-generic-type-using-referencetypedeserializer-with-jackson-spring
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.jsontype.TypeSerializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.ReferenceTypeSerializer
import com.fasterxml.jackson.databind.type.ReferenceType
import com.fasterxml.jackson.databind.util.NameTransformer
class PatchableModule: SimpleModule() {
override fun setupModule(context: SetupContext?) {
super.setupModule(context)
addDeserializer(Patchable::class.java, PatchableDeserializer())
// addSerializer(Patchable::class.java, PatchableSerializer::class)
}
}
sealed class Patchable<T> {
class Undefined<T>: Patchable<T>()
class Null<T>: Patchable<T>()
data class Present<T>(val content: T): Patchable<T>()
@JsonValue
fun value():T? =
when(this) {
is Undefined->null
is Null-> null
is Present -> content
}
companion object {
// fun undefined() = Undefined()
}
}
class PatchableDeserializer(): JsonDeserializer<Patchable<*>>(), ContextualDeserializer {
private var valueType: Class<*>? = null
constructor(valueType: Class<*>? = null):this() {
this.valueType=valueType
}
override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> {
val wrapperType = property?.type
val rawClass = wrapperType?.containedType(0)?.rawClass
return PatchableDeserializer(rawClass)
}
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Patchable<*> {
val f=p!!.readValueAs(valueType)
return Patchable.Present(f)
}
override fun getNullValue(ctxt: DeserializationContext?): Patchable<Any> =
if (ctxt?.parser?.currentToken == JsonToken.VALUE_NULL)
Patchable.Null()
//Patchable.ofNull()
else Patchable.Undefined()
//Patchable.undefined()
}
class PatchableSerializer : ReferenceTypeSerializer<Patchable<*>> // since 2.9
{
protected constructor(fullType: ReferenceType, staticTyping: Boolean,
vts: TypeSerializer, ser: JsonSerializer<Any>) : super(fullType, staticTyping, vts, ser) {
}
protected constructor(base: PatchableSerializer, property: BeanProperty,
vts: TypeSerializer, valueSer: JsonSerializer<*>, unwrapper: NameTransformer,
suppressableValue: Any, suppressNulls: Boolean) : super(base, property, vts, valueSer, unwrapper,
suppressableValue, suppressNulls) {
}
override fun withResolved(prop: BeanProperty,
vts: TypeSerializer, valueSer: JsonSerializer<*>,
unwrapper: NameTransformer): ReferenceTypeSerializer<Patchable<*>> {
return PatchableSerializer(this, prop, vts, valueSer, unwrapper,
_suppressableValue, _suppressNulls)
}
override fun withContentInclusion(suppressableValue: Any,
suppressNulls: Boolean): ReferenceTypeSerializer<Patchable<*>> {
return PatchableSerializer(this, _property, _valueTypeSerializer,
_valueSerializer, _unwrapper,
suppressableValue, suppressNulls)
}
override fun _isValuePresent(value: Patchable<*>): Boolean {
return value is Patchable.Present
}
override fun _getReferenced(value: Patchable<*>): Any {
return when(value) {
is Patchable.Present -> value.content !!
else -> error("foo")
}
}
override fun _getReferencedIfPresent(value: Patchable<*>): Any? {
return when(value) {
is Patchable.Present -> value.content
is Patchable.Null-> null
else-> null
}
}
companion object {
private val serialVersionUID = 1L
}
}
package com.example.config
import com.example.Patchable
import com.example.api.ApiConfig
import com.fasterxml.classmate.TypeResolver
import com.google.common.base.Predicates
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import springfox.documentation.builders.RequestHandlerSelectors
import springfox.documentation.schema.WildcardType
import springfox.documentation.spring.web.plugins.Docket
import springfox.documentation.swagger2.annotations.EnableSwagger2
import java.util.*
@Configuration
@EnableSwagger2
class Swagger(private val apiConfig: ApiConfig, private val typeResolver: TypeResolver) {
@Bean
fun mainApi(): Docket = apiConfig.toDocket()
.groupName("Main")
.select()
.apis(RequestHandlerSelectors.basePackage(apiConfig.getBasePackageName()))
.build()
//.additionalModels(typeResolver.resolve(Patchable::class.java, WildcardType::class.java))
// see: https://github.com/swagger-api/swagger-codegen/issues/7601
.genericModelSubstitutes(Optional::class.java)
.genericModelSubstitutes(Patchable::class.java)
@Bean
fun monitoringApi(): Docket = apiConfig.toDocket()
.groupName("Monitoring")
.useDefaultResponseMessages(true)
.select()
.apis(Predicates.not(RequestHandlerSelectors.basePackage(apiConfig.getBasePackageName())))
.build()
}
private fun ApiConfig.getBasePackageName() = this::class.java.`package`.name
private fun ApiConfig.toApiInfo() = springfox.documentation.builders.ApiInfoBuilder().title(this.title).build()
private fun ApiConfig.toDocket() = springfox.documentation.spring.web.plugins.Docket(springfox.documentation.spi.DocumentationType.SWAGGER_2).apiInfo(this.toApiInfo())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment