Last active
April 23, 2019 06:45
-
-
Save bastman/85cf7d802db23d0585cc87d1ee1d88ed to your computer and use it in GitHub Desktop.
kotlin jackson patchable deserializer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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