Last active
March 8, 2024 22:55
-
-
Save gagarski/c08566f6ed850f98d1c4b2d289d5b230 to your computer and use it in GitHub Desktop.
Jackson polymorphism (https://stackoverflow.com/questions/78004350/jackson-polymorphism-use-two-mechanism-at-once)
This file contains 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
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) | |
} | |
} | |
} |
This file contains 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 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 | |
} | |
} |
This file contains 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
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