> it = copy.properties().iterator(); it.hasNext();) {
+ if (!mappedProperties.isKnownJacksonProperty(it.next().getKey())) {
+ it.remove();
+ }
+ }
+
+ return copy;
+
+ }).orElse(source);
+ }
+
/**
* Overwrites the identifier and version fields in the given request {@link ObjectNode} with the values of the
* persisted {@code target} before the body is deserialized. This makes sure clients cannot mutate identifier or
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedJacksonProperties.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedJacksonProperties.java
index dc4be882d..a92da4ba8 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedJacksonProperties.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedJacksonProperties.java
@@ -297,4 +297,38 @@ public boolean isExposedProperty(String name) {
return property != null ? property.isWritable() : anySetterFound;
}
+
+ /**
+ * Returns whether the given field name is known to Jackson — either as a mapped persistent property, as an unmapped
+ * Jackson property (e.g. a {@link org.springframework.data.annotation.Transient} field that Jackson can still
+ * deserialize), or because there is a catch-all {@link com.fasterxml.jackson.annotation.JsonAnySetter} method.
+ *
+ * This is used to strip fields from an incoming {@link tools.jackson.databind.node.ObjectNode} that Jackson has no
+ * knowledge of and would therefore attempt to set via reflection on inherited private fields (e.g. {@code _links}
+ * from {@link org.springframework.hateoas.RepresentationModel}), which causes an
+ * {@link UnsupportedOperationException}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @return {@literal true} if the field is known to Jackson and should be kept in the request body.
+ * @since 5.2
+ * @see GH-1726
+ */
+ public boolean isKnownJacksonProperty(String name) {
+
+ Assert.hasText(name, "Property name must not be null or empty");
+
+ if (ignoredPropertyNames.contains(name)) {
+ return false;
+ }
+
+ if (fieldNameToProperty.containsKey(name)) {
+ return true;
+ }
+
+ if (anySetterFound) {
+ return true;
+ }
+
+ return unmappedProperties.stream().anyMatch(p -> p.getName().equals(name));
+ }
}
diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java
index f50726c28..44ee618ba 100755
--- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java
+++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java
@@ -12,6 +12,8 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
+ *
+ * Modifications copyright (C) 2026 Steve Rutherford
*/
package org.springframework.data.rest.webmvc.json;
@@ -55,6 +57,7 @@
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.webmvc.mapping.Associations;
+import org.springframework.hateoas.RepresentationModel;
import org.springframework.util.ObjectUtils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -73,6 +76,7 @@
* @author Mathias Düsterhöft
* @author Ken Dombeck
* @author Thomas Mrozinski
+ * @author Steve Rutherford
*/
@ExtendWith(MockitoExtension.class)
class DomainObjectReaderUnitTests {
@@ -110,6 +114,7 @@ void setUp() {
mappingContext.getPersistentEntity(BugModel.class);
mappingContext.getPersistentEntity(ArrayListHolder.class);
mappingContext.getPersistentEntity(MapWrapper.class);
+ mappingContext.getPersistentEntity(RepresentationModelEntity.class);
mappingContext.afterPropertiesSet();
this.entities = new PersistentEntities(Collections.singleton(mappingContext));
@@ -1243,4 +1248,34 @@ public void setValues(Collection values) {
static class MapWrapper {
public Map map = new HashMap<>();
}
+
+ // GH-1726 - entity that extends RepresentationModel, which has a private "links" field
+ // that Jackson would try to set when "_links" is present in the request body.
+ @JsonAutoDetect(fieldVisibility = Visibility.ANY)
+ static class RepresentationModelEntity extends RepresentationModel {
+
+ @Id Long id;
+ String name;
+ }
+
+ @Test // GH-1726
+ void readPutWithLinksFieldDoesNotThrowForRepresentationModelSubclass() throws Exception {
+
+ RepresentationModelEntity existing = new RepresentationModelEntity();
+ existing.id = 1L;
+ existing.name = "original";
+
+ ObjectMapper mapper = new ObjectMapper();
+ // Simulate a client sending back a response body that includes "_links" (as HAL clients typically do)
+ ObjectNode node = (ObjectNode) mapper.readTree(
+ "{ \"name\" : \"updated\", \"_links\" : { \"self\" : { \"href\" : \"http://localhost/entities/1\" } } }");
+
+ // Before the fix this would throw UnsupportedOperationException because Jackson tried to set
+ // the private "links" field on RepresentationModel via its unmodifiable list setter.
+ assertThatCode(() -> reader.readPut(node, existing, mapper)).doesNotThrowAnyException();
+
+ RepresentationModelEntity result = reader.readPut(node, existing, mapper);
+ assertThat(result.name).isEqualTo("updated");
+ assertThat(result.id).isEqualTo(1L);
+ }
}