Skip to content

Instantly share code, notes, and snippets.

@gagarski
Last active March 8, 2024 22:55
Show Gist options
  • Save gagarski/c08566f6ed850f98d1c4b2d289d5b230 to your computer and use it in GitHub Desktop.
Save gagarski/c08566f6ed850f98d1c4b2d289d5b230 to your computer and use it in GitHub Desktop.
import com.fasterxml.jackson.annotation.JsonTypeInfo.As
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.core.util.JsonParserSequence
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.jsontype.NamedType
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver
import com.fasterxml.jackson.databind.jsontype.impl.AsDeductionTypeDeserializer
import com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver
import com.fasterxml.jackson.databind.util.TokenBuffer
/**
* [AsPropertyTypeDeserializer] which falls back to type deduction for remaining types
* if there is a multiple candidates with same type name.
*/
class AsPropertyTypeWithDeductionDeserializer(
bt: JavaType,
subtypes: Collection<NamedType>,
config: DeserializationConfig,
idRes: TypeIdResolver,
typePropertyName: String,
typeIdVisible: Boolean,
defaultImpl: JavaType?,
inclusion: As,
strictTypeIdHandling: Boolean,
subTypeValidator: PolymorphicTypeValidator
) : AsPropertyTypeDeserializer(
bt,
idRes,
typePropertyName,
typeIdVisible,
defaultImpl,
inclusion,
strictTypeIdHandling
) {
private val deducers = getDeducers(
bt = bt,
subtypes = subtypes,
defaultImpl = defaultImpl,
config = config,
subTypeValidator = subTypeValidator
)
override fun _deserializeTypedForId(
p: JsonParser,
ctxt: DeserializationContext,
tb: TokenBuffer?,
typeId: String?
): Any {
val tb = if (_typeIdVisible) {
(tb ?: ctxt.bufferForInputBuffering(p))?.apply {
writeFieldName(p.currentName())
writeString(typeId)
}
} else {
tb
}
val p = if (tb != null) {
p.clearCurrentToken()
JsonParserSequence.createFlattened(false, tb.asParser(p), p)
} else {
p
}
if (p.currentToken() != JsonToken.END_OBJECT) {
// Must point to the next value; tb had no current, p pointed to VALUE_STRING:
p.nextToken() // to skip past String value
}
val deducer = deducers[typeId] ?: return _deserializeTypedUsingDefaultImpl(p, ctxt, tb, _msgForMissingId)
return deducer.deserializeTypedFromAny(p, ctxt)
}
companion object {
private fun getDeducers(
bt: JavaType,
subtypes: Collection<NamedType>,
defaultImpl: JavaType?,
config: DeserializationConfig,
subTypeValidator: PolymorphicTypeValidator
): Map<String, AsDeductionTypeDeserializer> {
val idRes = ClassNameIdResolver.construct(bt, config, subTypeValidator)
return subtypes
.groupBy {
it.getId(config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES))
}.mapValues { (k, v) ->
AsDeductionTypeDeserializer(bt, idRes, defaultImpl, config, v)
}
}
private fun NamedType.getId(caseInsensitive: Boolean): String {
// no name? Need to figure out default; for now, let's just
// use non-qualified class name
val cls: Class<*> = getType()
val id = if (hasName()) getName() else defaultTypeId(cls)
return if (caseInsensitive) id.lowercase() else id
}
private fun defaultTypeId(cls: Class<*>): String {
val n = cls.name
val ix = n.lastIndexOf('.')
return if ((ix < 0)) n else n.substring(ix + 1)
}
}
}
package ski.gagar.vertigram.util.jackson.typing
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.introspect.AnnotatedClass
import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver
import com.fasterxml.jackson.databind.jsontype.NamedType
import com.fasterxml.jackson.databind.jsontype.impl.StdSubtypeResolver
import java.lang.reflect.Modifier
/**
* Hacked version of [StdSubtypeResolver] which overrides [collectAndResolveSubtypesByTypeId] so
* that it returns the result with duplicates
*/
class StdSubtypeResolverWithDuplicates : StdSubtypeResolver {
constructor() : super()
constructor(std: StdSubtypeResolver) : super(std)
/**
* Monkey-patched version of [collectAndResolveSubtypesByTypeId] from superclass
* that returns a [Collection] of [NamedType] with duplicates by name if there are any
*/
override fun collectAndResolveSubtypesByTypeId(
config: MapperConfig<*>,
baseType: AnnotatedClass
): Collection<NamedType> {
val rawBase = baseType.rawType
val typesHandled: MutableSet<Class<*>> = mutableSetOf()
val collected: MutableSet<NamedType> = mutableSetOf()
val rootType = NamedType(rawBase, null)
_collectAndResolveByTypeId(baseType, rootType, config, typesHandled, collected)
for (subtype in _registeredSubtypes.orEmpty()) {
if (rawBase.isAssignableFrom(subtype.type)) {
val curr = AnnotatedClassResolver.resolveWithoutSuperTypes(
config,
subtype.type
)
_collectAndResolveByTypeId(curr, subtype, config, typesHandled, collected)
}
}
return _combineNamedAndUnnamed(rawBase, typesHandled, collected)
}
private fun _collectAndResolveByTypeId(
annotatedType: AnnotatedClass?, namedType: NamedType,
config: MapperConfig<*>,
typesHandled: MutableSet<Class<*>>, collected: MutableSet<NamedType>
) {
val ai = config.annotationIntrospector
@Suppress("NAME_SHADOWING")
val namedType =
if (namedType.hasName())
namedType
else {
val name = ai.findTypeName(annotatedType)
if (name != null)
NamedType(namedType.type, name)
else
namedType
}
if (namedType.hasName()) {
collected.add(namedType)
}
if (typesHandled.add(namedType.type)) {
for (subtype in ai.findSubtypes(annotatedType).orEmpty()) {
val subtypeClass = AnnotatedClassResolver.resolveWithoutSuperTypes(
config,
subtype.type
)
_collectAndResolveByTypeId(subtypeClass, subtype, config, typesHandled, collected)
}
}
}
private fun _combineNamedAndUnnamed(
rawBase: Class<*>,
typesHandled: MutableSet<Class<*>>, collected: MutableSet<NamedType>
): Collection<NamedType> {
val result = ArrayList(collected)
for (t in collected) {
typesHandled.remove(t.type)
}
for (cls in typesHandled) {
if ((cls == rawBase) && Modifier.isAbstract(cls.modifiers)) {
continue
}
result.add(NamedType(cls))
}
return result
}
}
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.jsontype.NamedType
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer
import com.fasterxml.jackson.databind.jsontype.impl.StdSubtypeResolver
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder
/**
* [StdTypeResolverBuilder] which overrides [buildTypeDeserializer] returning [AsPropertyTypeWithDeductionDeserializer].
*
* Currently, supports only [JsonTypeInfo.Id.NAME] with combination of
* [JsonTypeInfo.As.PROPERTY] or [JsonTypeInfo.As.EXISTING_PROPERTY]
*/
class TypeResolverWithDeductionBuilder : StdTypeResolverBuilder {
constructor() : super()
private constructor(base: TypeResolverWithDeductionBuilder, defaultImpl: Class<*>) : super(base, defaultImpl)
override fun withDefaultImpl(defaultImpl: Class<*>): TypeResolverWithDeductionBuilder {
if (_defaultImpl == defaultImpl) {
return this
}
return TypeResolverWithDeductionBuilder(this, defaultImpl)
}
override fun withSettings(settings: JsonTypeInfo.Value): StdTypeResolverBuilder {
checkId(settings.idType)
checkAs(settings.inclusionType)
return super.withSettings(settings)
}
override fun buildTypeDeserializer(
config: DeserializationConfig,
baseType: JavaType,
subtypes: MutableCollection<NamedType>
): TypeDeserializer? {
if (baseType.isPrimitive) {
if (!allowPrimitiveTypes(config, baseType)) {
return null
}
}
val bean = config.introspectClassAnnotations(baseType.rawClass)
val ac = bean.classInfo
@Suppress("NAME_SHADOWING")
val subtypes = StdSubtypeResolverWithDuplicates(config.subtypeResolver as StdSubtypeResolver)
.collectAndResolveSubtypesByTypeId(config, ac)
val subTypeValidator = verifyBaseTypeValidity(config, baseType)
val idRes = idResolver(config, baseType, subTypeValidator, subtypes, false, true)
val defaultImpl = defineDefaultImpl(config, baseType)
return AsPropertyTypeWithDeductionDeserializer(
bt = baseType,
subtypes = subtypes,
config = config,
idRes = idRes,
defaultImpl = defaultImpl,
typePropertyName = _typeProperty,
typeIdVisible = _typeIdVisible,
inclusion = _includeAs,
strictTypeIdHandling = _strictTypeIdHandling(config, baseType),
subTypeValidator = subTypeValidator
)
}
companion object {
fun checkId(id: JsonTypeInfo.Id) = id.also {
require(id == JsonTypeInfo.Id.NAME) {
"$id type info id is unsupported"
}
}
fun checkAs(`as`: JsonTypeInfo.As) = `as`.also {
require(`as` == JsonTypeInfo.As.PROPERTY || `as` == JsonTypeInfo.As.EXISTING_PROPERTY)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment