diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 26d35b72f..4a2c48c29 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -19,6 +19,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import java.io.IOException; +import java.lang.reflect.Field; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -57,6 +58,7 @@ import org.springframework.hateoas.Resource; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.mvc.ResourceProcessorInvoker; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonUnwrapped; @@ -75,11 +77,13 @@ import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder; import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.CreatorProperty; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -414,6 +418,8 @@ public class PersistentEntityJackson2Module extends SimpleModule { public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) { + ValueInstantiatorCustomizer customizer = new ValueInstantiatorCustomizer(builder.getValueInstantiator(), config); + Iterator properties = builder.getProperties(); entities.getPersistentEntity(beanDesc.getBeanClass()).ifPresent(entity -> { @@ -446,11 +452,79 @@ public class PersistentEntityJackson2Module extends SimpleModule { UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(persistentProperty, converter); JsonDeserializer deserializer = wrapIfCollection(persistentProperty, uriStringDeserializer, config); - builder.addOrReplaceProperty(property.withValueDeserializer(deserializer), false); + customizer.replacePropertyIfNeeded(builder, property.withValueDeserializer(deserializer)); } }); - return builder; + return customizer.conclude(builder); + } + + /** + * Advanced customization of the {@link CreatorProperty} instances customized to additionally register them with the + * {@link ValueInstantiator} backing the {@link BeanDeserializerModifier}. This is necessary as the standard + * customization does not propagate into the initial object construction as the {@link CreatorProperty} instances + * for that are looked up via the {@link ValueInstantiator} and the property model behind those is not undergoing + * the customization currently (Jackson 2.9.9). + * + * @author Oliver Drotbohm + * @see https://github.com/FasterXML/jackson-databind/issues/2367 + */ + static class ValueInstantiatorCustomizer { + + private final SettableBeanProperty[] properties; + private final StdValueInstantiator instantiator; + + ValueInstantiatorCustomizer(ValueInstantiator instantiator, DeserializationConfig config) { + + this.instantiator = StdValueInstantiator.class.isInstance(instantiator) // + ? StdValueInstantiator.class.cast(instantiator) // + : null; + + this.properties = this.instantiator == null || this.instantiator.getFromObjectArguments(config) == null // + ? new SettableBeanProperty[0] // + : this.instantiator.getFromObjectArguments(config).clone(); // + } + + /** + * Replaces the logically same property with the given {@link SettableBeanProperty} on the given + * {@link BeanDeserializerBuilder}. In case we get a {@link CreatorProperty} we als register that one to be later + * exposed via the {@link ValueInstantiator} backing the {@link BeanDeserializerBuilder}. + * + * @param builder must not be {@literal null}. + * @param property must not be {@literal null}. + */ + void replacePropertyIfNeeded(BeanDeserializerBuilder builder, SettableBeanProperty property) { + + builder.addOrReplaceProperty(property, false); + + if (!CreatorProperty.class.isInstance(property)) { + return; + } + + properties[((CreatorProperty) property).getCreatorIndex()] = property; + } + + /** + * Concludes the setup of the given {@link BeanDeserializerBuilder} by reflectively registering the potentially + * customized {@link SettableBeanProperty} instances in the {@link ValueInstantiator} backing the builder. + * + * @param builder must not be {@literal null}. + * @return + */ + BeanDeserializerBuilder conclude(BeanDeserializerBuilder builder) { + + if (instantiator == null) { + return builder; + } + + Field field = ReflectionUtils.findField(StdValueInstantiator.class, "_constructorArguments"); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, instantiator, properties); + + builder.setValueInstantiator(instantiator); + + return builder; + } } private static JsonDeserializer wrapIfCollection(PersistentProperty property, diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java index 81da1a622..e82452149 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java @@ -89,6 +89,7 @@ public class PersistentEntityJackson2ModuleUnitTests { mappingContext.getPersistentEntity(Sample.class); mappingContext.getPersistentEntity(SampleWithAdditionalGetters.class); mappingContext.getPersistentEntity(PersistentEntityJackson2ModuleUnitTests.PetOwner.class); + mappingContext.getPersistentEntity(Immutable.class); this.persistentEntities = new PersistentEntities(Arrays.asList(mappingContext)); @@ -181,6 +182,21 @@ public class PersistentEntityJackson2ModuleUnitTests { assertThat(JsonPath.parse(result).read("$.home", Integer.class)) // .isEqualTo(41); } + @Test // DATAREST-1393 + + public void customizesDeserializerForCreatorProperties() throws Exception { + + PersistentProperty property = persistentEntities // + .getRequiredPersistentEntity(Immutable.class) // + .getRequiredPersistentProperty("home"); + + when(associations.isLinkableAssociation(property)).thenReturn(true); + + mapper.readValue("{ \"home\" : \"homes:4711\"}", Immutable.class); + + verify(converter).convert(URI.create("homes:4711"), TypeDescriptor.valueOf(URI.class), + TypeDescriptor.valueOf(Home.class)); + } /** * @author Oliver Gierke @@ -228,4 +244,13 @@ public class PersistentEntityJackson2ModuleUnitTests { return 5; } } + + static class Immutable { + + private final @SuppressWarnings("unused") Home home; + + public Immutable(@JsonProperty("home") Home home) { + this.home = home; + } + } }