Skip to content

Commit

Permalink
feat: JsonUnwrappedDeserializer supports multiple polymorphic unwrapp…
Browse files Browse the repository at this point in the history
…ed fields

Signed-off-by: Marc Nuri <[email protected]>
  • Loading branch information
manusa authored Apr 16, 2024
1 parent 602aba1 commit aad0cd5
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,22 @@
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TreeTraversingParser;
import com.fasterxml.jackson.databind.util.NameTransformer;

import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Desc: this is a workaround on the problem that Jackson's @JsonUnwrapped doesn't work with
Expand All @@ -45,7 +51,7 @@
*/
public class JsonUnwrappedDeserializer<T> extends JsonDeserializer<T> implements ContextualDeserializer {

private static JsonUnwrapped cancelUnwrappedAnnotation;
private static final JsonUnwrapped cancelUnwrappedAnnotation;

static {
try {
Expand All @@ -58,8 +64,7 @@ public class JsonUnwrappedDeserializer<T> extends JsonDeserializer<T> implements

private JsonDeserializer<T> beanDeserializer;
private Set<String> ownPropertyNames;
private String unwrappedPropertyName;
private NameTransformer nameTransformer;
private List<UnwrappedInfo> unwrappedInfos;

/*
* Needed by Jackson
Expand All @@ -72,15 +77,12 @@ public JsonUnwrappedDeserializer(DeserializationContext deserializationContext)

BeanDescription description = deserializationContext.getConfig().introspect(type);

final JsonUnwrapped[] tempUnwrappedAnnotation = { null };

List<BeanPropertyDefinition> unwrappedProperties = description.findProperties().stream()
.filter(prop -> Arrays.asList(prop.getConstructorParameter(), prop.getMutator(), prop.getField()).stream()
.filter(prop -> Stream.of(prop.getConstructorParameter(), prop.getMutator(), prop.getField())
.filter(Objects::nonNull)
.anyMatch(member -> {
JsonUnwrapped unwrappedAnnotation = member.getAnnotation(JsonUnwrapped.class);
if (unwrappedAnnotation != null) {
tempUnwrappedAnnotation[0] = unwrappedAnnotation;
member.getAllAnnotations().add(cancelUnwrappedAnnotation);
}
return unwrappedAnnotation != null;
Expand All @@ -89,25 +91,57 @@ public JsonUnwrappedDeserializer(DeserializationContext deserializationContext)

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 = description.findProperties().stream()
.map(BeanPropertyDefinition::getName)
.collect(Collectors.toSet());
ownPropertyNames.removeAll(description.getIgnoredPropertyNames());

JsonDeserializer<Object> rawBeanDeserializer = deserializationContext.getFactory()
.createBeanDeserializer(deserializationContext, type, description);
((ResolvableDeserializer) rawBeanDeserializer).resolve(deserializationContext);
beanDeserializer = (JsonDeserializer<T>) rawBeanDeserializer;

unwrappedInfos = new ArrayList<>();
for (BeanPropertyDefinition unwrappedProperty : unwrappedProperties) {
unwrappedInfos.add(new UnwrappedInfo(deserializationContext, unwrappedProperty));
ownPropertyNames.remove(unwrappedProperty.getName());
}
}

private static final class UnwrappedInfo {
final String propertyName;
final NameTransformer nameTransformer;
final Set<String> beanPropertyNames;

public UnwrappedInfo(DeserializationContext context, BeanPropertyDefinition unwrappedProperty) {
propertyName = unwrappedProperty.getName();
final JsonUnwrapped annotation = unwrappedProperty.getField().getAnnotation(JsonUnwrapped.class);
nameTransformer = NameTransformer.simpleTransformer(annotation.prefix(), annotation.suffix());
beanPropertyNames = new HashSet<>();
// Extract viable property names for deserialization and nested deserialization
final Set<Class<?>> processedTypes = new HashSet<>();
extractPropertiesDeep(context, processedTypes, beanPropertyNames, unwrappedProperty);
}

private static void extractPropertiesDeep(DeserializationContext context, Set<Class<?>> processedTypes,
Set<String> properties, BeanPropertyDefinition bean) {
final Collection<NamedType> types = context.getConfig().getSubtypeResolver()
.collectAndResolveSubtypesByClass(context.getConfig(),
context.getConfig().introspect(bean.getPrimaryType()).getClassInfo());
for (NamedType type : types) {
if (!processedTypes.add(type.getType())) {
continue;
}
for (BeanPropertyDefinition property : context.getConfig().introspect(context.constructType(type.getType()))
.findProperties()) {
properties.add(property.getName());
extractPropertiesDeep(context, processedTypes, properties, property);
}
}
}

}

@Override
Expand All @@ -118,25 +152,32 @@ public JsonDeserializer<?> createContextual(DeserializationContext deserializati

@Override
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
ObjectNode node = jsonParser.readValueAsTree();

ObjectNode ownNode = deserializationContext.getNodeFactory().objectNode();
ObjectNode unwrappedNode = deserializationContext.getNodeFactory().objectNode();
final ObjectNode node = jsonParser.readValueAsTree();
final ObjectNode ownNode = deserializationContext.getNodeFactory().objectNode();
final Map<UnwrappedInfo, ObjectNode> unwrappedNodes = new HashMap<>();

node.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();

String transformed = nameTransformer.reverse(key);

if (transformed != null && !ownPropertyNames.contains(key)) {
unwrappedNode.replace(transformed, value);
} else {
final String key = entry.getKey();
final JsonNode value = entry.getValue();

boolean replaced = false;
for (UnwrappedInfo unwrapped : unwrappedInfos) {
final ObjectNode unwrappedNode = unwrappedNodes.computeIfAbsent(unwrapped,
k -> deserializationContext.getNodeFactory().objectNode());
final String transformed = unwrapped.nameTransformer.reverse(key);
if (transformed != null && !ownPropertyNames.contains(key) && unwrapped.beanPropertyNames.contains(transformed)) {
unwrappedNode.replace(transformed, value);
replaced = true;
}
}
if (!replaced && ownPropertyNames.contains(key)) {
ownNode.replace(key, value);
}
});

ownNode.replace(unwrappedPropertyName, unwrappedNode);
for (Map.Entry<UnwrappedInfo, ObjectNode> entry : unwrappedNodes.entrySet()) {
ownNode.replace(entry.getKey().propertyName, entry.getValue());
}

try (TreeTraversingParser syntheticParser = new TreeTraversingParser(ownNode, jsonParser.getCodec())) {
syntheticParser.nextToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,150 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.DEDUCTION;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NONE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class JsonUnwrappedDeserializerTest {

private static final String EXPECTED_VALUE_A = "Value A";
private static final String EXPECTED_VALUE_B = "Value B";
private static final String EXPECTED_VALUE_C = "Value C";

@Test
void shouldDeserializeInterfacesWithJsonWrapped() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
RootClass instance = mapper.readValue("{ \"stringField\": \"" + EXPECTED_VALUE_A + "\", "
+ "\"extendedField\": \"" + EXPECTED_VALUE_B + "\", "
+ "\"nestedField\": \"" + EXPECTED_VALUE_C + "\" }", RootClass.class);
// Verify normal fields works along to the json-wrapped fields
assertEquals(EXPECTED_VALUE_A, instance.stringField);

// Verify interfaces are supported at root level
assertNotNull(instance.rootInterface, "Interface was not deserialized!");
assertTrue(instance.rootInterface instanceof RootImplementation);
RootImplementation rootImplementation = ((RootImplementation) instance.rootInterface);
assertEquals(EXPECTED_VALUE_B, rootImplementation.extendedField);

// Verify nested interfaces are also supported
assertTrue(rootImplementation.nestedInterface instanceof NestedImplementation);
assertEquals(EXPECTED_VALUE_C, ((NestedImplementation) rootImplementation.nestedInterface).nestedField);
private ObjectMapper mapper;

@BeforeEach
void initMapper() {
mapper = new ObjectMapper();
}

@Nested
class Deserialize {

@Test
@DisplayName("Single @JsonUnwrapped polymorphic type")
void singleInterfaceWithJsonWrapped() throws JsonProcessingException {
RootClass instance = mapper.readValue("{ \"stringField\": \"" + EXPECTED_VALUE_A + "\", "
+ "\"extendedField\": \"" + EXPECTED_VALUE_B + "\", "
+ "\"nestedField\": \"" + EXPECTED_VALUE_C + "\" }", RootClass.class);
// Verify normal fields works along to the json-wrapped fields
assertEquals(EXPECTED_VALUE_A, instance.stringField);

// Verify interfaces are supported at root level
assertNotNull(instance.rootInterface, "Interface was not deserialized!");
assertInstanceOf(RootImplementation.class, instance.rootInterface);
RootImplementation rootImplementation = ((RootImplementation) instance.rootInterface);
assertEquals(EXPECTED_VALUE_B, rootImplementation.extendedField);

// Verify nested interfaces are also supported
assertInstanceOf(NestedImplementation.class, rootImplementation.nestedInterface);
assertEquals(EXPECTED_VALUE_C, ((NestedImplementation) rootImplementation.nestedInterface).nestedField);
}

@Test
@DisplayName("Multiple @JsonUnwrapped fields")
void multipleJsonUnwrappedFields() throws JsonProcessingException {
final MultipleJsonUnwrapped result = mapper.readValue("{" +
"\"foo\": \"foo-value\"," +
"\"bar\": \"bar-value\"," +
"\"control\": \"pass\"" +
"}", MultipleJsonUnwrapped.class);
assertThat(result)
.hasFieldOrPropertyWithValue("foo.foo", "foo-value")
.hasFieldOrPropertyWithValue("bar.bar", "bar-value")
.hasFieldOrPropertyWithValue("control", "pass");
}

@Test
@DisplayName("Multiple polymorphic fields")
void multiplePolymorphicFields() throws JsonProcessingException {
final MultiplePolymorphicFields result = mapper.readValue("{" +
"\"foo\": {\"foo\": \"foo-value\"}," +
"\"bar\": {\"bar\": \"bar-value\"}," +
"\"control\": \"pass\"" +
"}", MultiplePolymorphicFields.class);
assertThat(result)
.hasFieldOrPropertyWithValue("foo.foo", "foo-value")
.hasFieldOrPropertyWithValue("bar.bar", "bar-value")
.hasFieldOrPropertyWithValue("control", "pass");
}

@Test
@DisplayName("Multiple @JsonUnwrapped polymorphic fields")
void multipleJsonUnwrappedPolymorphicFields() throws JsonProcessingException {
final MultipleJsonUnwrappedPolymorphicFields result = mapper.readValue("{" +
"\"foo\": \"foo-value\"," +
"\"bar\": \"bar-value\"," +
"\"control\": \"pass\"" +
"}", MultipleJsonUnwrappedPolymorphicFields.class);
assertThat(result)
.hasFieldOrPropertyWithValue("foo.foo", "foo-value")
.hasFieldOrPropertyWithValue("bar.bar", "bar-value")
.hasFieldOrPropertyWithValue("control", "pass");
}

}

@Data
public static class MultipleJsonUnwrapped {
@JsonUnwrapped
private FooImpl foo;
@JsonUnwrapped
private BarImpl bar;
private String control;
}

@Data
public static class MultiplePolymorphicFields {
private Foo foo;
private Bar bar;
private String control;
}

@Data
@JsonDeserialize(using = io.fabric8.kubernetes.model.jackson.JsonUnwrappedDeserializer.class)
public static class MultipleJsonUnwrappedPolymorphicFields {
@JsonUnwrapped
private Foo foo;
@JsonUnwrapped
private Bar bar;
private String control;
}

@JsonSubTypes(@JsonSubTypes.Type(FooImpl.class))
@JsonTypeInfo(use = DEDUCTION)
public interface Foo {
String getFoo();
}

@JsonSubTypes(@JsonSubTypes.Type(BarImpl.class))
@JsonTypeInfo(use = DEDUCTION)
public interface Bar {
String getBar();
}

@Data
@NoArgsConstructor
@JsonTypeInfo(use = NONE)
public static class FooImpl implements Foo {
private String foo;
}

@Data
@NoArgsConstructor
@JsonTypeInfo(use = NONE)
public static class BarImpl implements Bar {
private String bar;
}

@JsonDeserialize(using = io.fabric8.kubernetes.model.jackson.JsonUnwrappedDeserializer.class)
Expand Down Expand Up @@ -83,7 +197,7 @@ public void setRootInterface(RootInterface rootInterface) {
}

@JsonSubTypes(@JsonSubTypes.Type(RootImplementation.class))
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonTypeInfo(use = DEDUCTION)
interface RootInterface {

}
Expand All @@ -109,7 +223,7 @@ public void setExtendedField(String extendedField) {
}

@JsonSubTypes(@JsonSubTypes.Type(NestedImplementation.class))
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonTypeInfo(use = DEDUCTION)
interface NestedInterface {

}
Expand Down

0 comments on commit aad0cd5

Please sign in to comment.