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 314aab395..bef961f0c 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 @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import org.slf4j.Logger; @@ -82,6 +83,8 @@ 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.introspect.AnnotatedField; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -410,6 +413,7 @@ private Object toModel(Object value, SerializerProvider provider) throws JsonMap * non-optional associations can be populated on resource creation. * * @author Oliver Gierke + * @author Lars Vierbergen */ public static class AssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier { @@ -444,7 +448,18 @@ public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanD while (properties.hasNext()) { SettableBeanProperty property = properties.next(); - PersistentProperty persistentProperty = entity.getPersistentProperty(property.getName()); + // To find the PersistentProperty name in case there is a @JsonProperty annotation + // on the field. Both BeanPropertyDefinition#getName() and BeanPropertyDefinition#getInternalName() + // don't return the actual name of the field, so we look up the AnnotatedField itself to retrieve + // the real name from, so it can be used for PersistentProperty lookup + String persistentPropertyName = beanDesc.findProperties().stream() + .filter(propertyDefinition -> property.getName().equals(propertyDefinition.getName())) + .map(BeanPropertyDefinition::getField).filter(Objects::nonNull).map(AnnotatedField::getName).findFirst() + // Fall back to the JSON name in case we can't find a BeanPropertyDefinition, + // so things can be mapped by convention in case they are immutable objects and are + // using constructor injection + .orElse(property.getName()); + PersistentProperty persistentProperty = entity.getPersistentProperty(persistentPropertyName); if (persistentProperty == null) { continue; 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 640e1d62a..678dc36d0 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 @@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import lombok.AccessLevel; import lombok.Data; import lombok.Getter; @@ -96,6 +97,7 @@ void setUp() { KeyValueMappingContext mappingContext = new KeyValueMappingContext<>(); mappingContext.getPersistentEntity(Sample.class); + mappingContext.getPersistentEntity(Package.class); mappingContext.getPersistentEntity(SampleWithAdditionalGetters.class); mappingContext.getPersistentEntity(PersistentEntityJackson2ModuleUnitTests.PetOwner.class); mappingContext.getPersistentEntity(Immutable.class); @@ -157,6 +159,41 @@ void resolvesReferenceToSubtypeCorrectly() throws IOException { assertThat(petOwner.getPet()).isNotNull(); } + @Test + void allowsUrlsForLinkableAssociation() throws Exception { + + when(converter.convert(UriTemplate.of("/homes/1").expand(), TypeDescriptor.valueOf(URI.class), + TypeDescriptor.valueOf(Home.class))).thenReturn(new Home()); + + PersistentProperty property = persistentEntities.getRequiredPersistentEntity(PetOwner.class) + .getRequiredPersistentProperty("home"); + + when(associations.isLinkableAssociation(property)).thenReturn(true); + + PetOwner petOwner = mapper.readValue("{\"home\": \"/homes/1\" }", PetOwner.class); + + assertThat(petOwner).isNotNull(); + assertThat(petOwner.getHome()).isInstanceOf(Home.class); + } + + @Test + void allowsUrlsForRenamedLinkableAssociation() throws IOException { + + when(converter.convert(UriTemplate.of("/packages/1").expand(), TypeDescriptor.valueOf(URI.class), + TypeDescriptor.valueOf(Package.class))).thenReturn(new Package()); + + PersistentProperty property = persistentEntities.getRequiredPersistentEntity(PetOwner.class) + .getRequiredPersistentProperty("_package"); + + when(associations.isLinkableAssociation(property)).thenReturn(true); + + PetOwner petOwner = mapper.readValue("{\"package\":\"/packages/1\"}", PetOwner.class); + + assertThat(petOwner).isNotNull(); + assertThat(petOwner.getPackage()).isNotNull(); + } + + @Test // DATAREST-1321 void allowsNumericIdsForLookupTypes() throws Exception { @@ -260,8 +297,17 @@ static class PetOwner { Pet pet; Home home; + + @Getter(value = AccessLevel.NONE) + @JsonProperty("package") Package _package; + + public Package getPackage() { + return _package; + } } + static class Package {} + @JsonTypeInfo(include = JsonTypeInfo.As.PROPERTY, use = JsonTypeInfo.Id.MINIMAL_CLASS) static class Pet {}