Last active
May 11, 2026 08:09
-
-
Save Anton3/348e639b6d46c3598f3311b9feca8578 to your computer and use it in GitHub Desktop.
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 name.anton3.vkapi.generator.json | |
| import com.fasterxml.jackson.annotation.JsonUnwrapped | |
| import com.fasterxml.jackson.core.JsonParser | |
| import com.fasterxml.jackson.databind.* | |
| import com.fasterxml.jackson.databind.deser.ContextualDeserializer | |
| import com.fasterxml.jackson.databind.deser.ResolvableDeserializer | |
| import com.fasterxml.jackson.databind.deser.std.StdDeserializer | |
| import com.fasterxml.jackson.databind.node.ObjectNode | |
| import com.fasterxml.jackson.databind.node.TreeTraversingParser | |
| import com.fasterxml.jackson.databind.util.NameTransformer | |
| class SinglePolyUnwrappedDeserializer<T : Any> : JsonDeserializer<T>(), ContextualDeserializer { | |
| override fun deserialize(p: JsonParser, ctxt: DeserializationContext): T = error("Not implemented") | |
| override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): JsonDeserializer<T> = | |
| SinglePolyUnwrappedDeserializerImpl(ctxt) | |
| } | |
| private class SinglePolyUnwrappedDeserializerImpl<T : Any>(ctxt: DeserializationContext) : | |
| StdDeserializer<T>(null as JavaType?) { | |
| private val type: JavaType = ctxt.contextualType | |
| private val beanDeserializer: JsonDeserializer<T> | |
| private val ownPropertyNames: Set<String> | |
| private val unwrappedType: JavaType | |
| private val unwrappedPropertyName: String | |
| private val nameTransformer: NameTransformer | |
| init { | |
| val description: BeanDescription = ctxt.config.introspect(type) | |
| var tempUnwrappedAnnotation: JsonUnwrapped? = null | |
| val unwrappedProperties = description.findProperties().filter { prop -> | |
| listOfNotNull(prop.constructorParameter, prop.mutator, prop.field).any { member -> | |
| val unwrappedAnnotation: JsonUnwrapped? = member.getAnnotation(JsonUnwrapped::class.java) | |
| if (unwrappedAnnotation != null) { | |
| tempUnwrappedAnnotation = unwrappedAnnotation | |
| member.allAnnotations.add(notUnwrappedAnnotation) | |
| } | |
| unwrappedAnnotation != null | |
| } | |
| } | |
| val unwrappedProperty = when (unwrappedProperties.size) { | |
| 0 -> error("@JsonUnwrapped properties not found in ${type.typeName}") | |
| 1 -> unwrappedProperties.single() | |
| else -> error("Multiple @JsonUnwrapped properties found in ${type.typeName}") | |
| } | |
| nameTransformer = tempUnwrappedAnnotation!!.run { NameTransformer.simpleTransformer(prefix, suffix) } | |
| unwrappedPropertyName = unwrappedProperty.name | |
| ownPropertyNames = description.findProperties().mapTo(mutableSetOf()) { it.name } | |
| ownPropertyNames.remove(unwrappedPropertyName) | |
| ownPropertyNames.removeAll(description.ignoredPropertyNames) | |
| unwrappedType = unwrappedProperty.primaryType | |
| val rawBeanDeserializer = ctxt.factory.createBeanDeserializer(ctxt, type, description) | |
| (rawBeanDeserializer as? ResolvableDeserializer)?.resolve(ctxt) | |
| @Suppress("UNCHECKED_CAST") | |
| beanDeserializer = rawBeanDeserializer as JsonDeserializer<T> | |
| } | |
| override fun deserialize(p: JsonParser, ctxt: DeserializationContext): T { | |
| val node = p.readValueAsTree<ObjectNode>() | |
| val ownNode = ObjectNode(ctxt.nodeFactory) | |
| val unwrappedNode = ObjectNode(ctxt.nodeFactory) | |
| node.fields().forEach { (key, value) -> | |
| val transformed: String? = nameTransformer.reverse(key) | |
| if (transformed != null && key !in ownPropertyNames) { | |
| unwrappedNode.replace(transformed, value) | |
| } else { | |
| ownNode.replace(key, value) | |
| } | |
| } | |
| ownNode.replace(unwrappedPropertyName, unwrappedNode) | |
| val syntheticParser = TreeTraversingParser(ownNode) | |
| syntheticParser.nextToken() | |
| return beanDeserializer.deserialize(syntheticParser, ctxt) | |
| } | |
| private class NotUnwrapped( | |
| @Suppress("unused") | |
| @field:JsonUnwrapped(enabled = false) | |
| @JvmField | |
| val dummy: Nothing | |
| ) | |
| companion object { | |
| val notUnwrappedAnnotation: JsonUnwrapped = | |
| NotUnwrapped::class.java.getField("dummy").getAnnotation(JsonUnwrapped::class.java) | |
| } | |
| } |
In case anyone stumbled upon the same issue, we just recently upgraded our project to java 25 and spring boot 4 and this deserializer stopped working because of the jackson 3 version and so on. It was parametrized for a specific class, with one custom statement regarding '@'type field for mapping purposes. Its not perfect but maybe helps someone save time. With the help of claude, here is a working version adjusted to the above-mentioned setup:
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.BeanDescription;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.deser.std.StdDeserializer;
import tools.jackson.databind.introspect.Annotated;
import tools.jackson.databind.introspect.AnnotatedMember;
import tools.jackson.databind.introspect.BeanPropertyDefinition;
import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.util.NameTransformer;
/**
* Workaround for Jackson's @JsonUnwrapped not working with polymorphism (@JsonTypeInfo).
* Adapted from https://stackoverflow.com/questions/37423848/deserializing-polymorphic-types-with-jsonunwrapped-using-jackson
* Based on java adaptation by https://github.com/author zhiwei.wei
from 2021/09/07 10:55
*/
public class SinglePolyUnwrappedDeserializer extends ValueDeserializer<YourDTO> {
@Override
public YourDTO deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {
throw new UnsupportedOperationException("not implemented");
}
private static class Impl extends StdDeserializer<YourDTO> {
private final ObjectMapper modifiedMapper;
private final Set<String> ownPropertyNames;
private final String unwrappedPropertyName;
private final NameTransformer nameTransformer;
public Impl(DeserializationContext deserializationContext, BeanProperty beanProperty) {
super(Objects.class);
JavaType type = deserializationContext.getContextualType();
BeanDescription description = deserializationContext.getConfig()
.classIntrospectorInstance().introspectForDeserialization(type,
deserializationContext.getConfig().classIntrospectorInstance().introspectClassAnnotations(type));
final JsonUnwrapped[] tempUnwrappedAnnotation = {null};
List<BeanPropertyDefinition> unwrappedProperties = description.findProperties().stream()
.filter(prop -> Arrays.asList(prop.getConstructorParameter(), prop.getMutator(), prop.getField())
.stream()
.filter(Objects::nonNull)
.anyMatch(member -> {
JsonUnwrapped ann = member.getAnnotation(JsonUnwrapped.class);
if (ann != null) {
tempUnwrappedAnnotation[0] = ann;
}
return ann != null;
}))
.collect(Collectors.toList());
if (unwrappedProperties.isEmpty()) {
throw new UnsupportedOperationException("@JsonUnwrapped properties not found in " + type.getTypeName());
} else if (unwrappedProperties.size() > 1) {
throw new UnsupportedOperationException("Multiple @JsonUnwrapped properties found in " + type.getTypeName());
}
BeanPropertyDefinition unwrappedProperty = unwrappedProperties.get(0);
nameTransformer = NameTransformer.simpleTransformer(
tempUnwrappedAnnotation[0].prefix(), tempUnwrappedAnnotation[0].suffix());
unwrappedPropertyName = unwrappedProperty.getName();
ownPropertyNames = description.findProperties().stream()
.map(BeanPropertyDefinition::getName)
.collect(Collectors.toSet());
ownPropertyNames.remove(unwrappedPropertyName);
ownPropertyNames.removeAll(description.getIgnoredPropertyNames());
JsonTypeInfo typeInfoAnnotation = type.getRawClass().getAnnotation(JsonTypeInfo.class);
if (typeInfoAnnotation != null && !typeInfoAnnotation.property().isEmpty()) {
ownPropertyNames.add(typeInfoAnnotation.property());
}
final String propName = unwrappedPropertyName;
final Class<?> rawClass = type.getRawClass();
modifiedMapper = JsonMapper.builder()
.annotationIntrospector(new JacksonAnnotationIntrospector() {
@Override
public NameTransformer findUnwrappingNameTransformer(
MapperConfig<?> config, AnnotatedMember member) {
if (rawClass.isAssignableFrom(member.getDeclaringClass())
&& propName.equals(member.getName())) {
return null; // cancel @JsonUnwrapped
}
return super.findUnwrappingNameTransformer(config, member);
}
@Override
public Object findDeserializer(MapperConfig<?> config, Annotated a) {
if (rawClass.isAssignableFrom(a.getRawType())) {
return null;
}
return super.findDeserializer(config, a);
}
})
.build();
}
@Override
public YourDTO deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {
JsonNode node = deserializationContext.readTree(jsonParser);
ObjectNode ownNode = deserializationContext.getNodeFactory().objectNode();
ObjectNode unwrappedNode = deserializationContext.getNodeFactory().objectNode();
node.properties().forEach(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
String transformed = nameTransformer.reverse(key);
if (transformed != null && !ownPropertyNames.contains(key) && !transformed.equals("@type")) {
unwrappedNode.replace(transformed, value);
} else {
ownNode.replace(key, value);
}
});
ownNode.replace(unwrappedPropertyName, unwrappedNode);
return modifiedMapper.treeToValue(ownNode, YourDTO.class);
}
}
@Override
public ValueDeserializer<YourDTO> createContextual(
DeserializationContext deserializationContext, BeanProperty beanProperty) {
return new Impl(deserializationContext, beanProperty);
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This deserializer saved me after 2 days of desperation. I can't believe that Jackson still doesn't support polymorphism on Unwrapped properties