diff --git a/pom.xml b/pom.xml
index 253b26b609..3ceeab1532 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-commons
- 4.1.0-SNAPSHOT
+ 4.1.0-GH-3400-SNAPSHOTSpring Data CoreCore Spring concepts underpinning every Spring Data module.
@@ -388,6 +388,28 @@
+
+ jmh
+
+
+ org.openjdk.jmh
+ jmh-core
+ test
+ ${jmh}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ test
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
diff --git a/src/jmh/java/org/springframework/data/BenchmarkSettings.java b/src/jmh/java/org/springframework/data/BenchmarkSettings.java
new file mode 100644
index 0000000000..5b7ae8752f
--- /dev/null
+++ b/src/jmh/java/org/springframework/data/BenchmarkSettings.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+package org.springframework.data;
+
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Global benchmark settings.
+ *
+ * @author Mark Paluch
+ */
+@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(value = 1, warmups = 0)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+public abstract class BenchmarkSettings {
+
+}
diff --git a/src/jmh/java/org/springframework/data/core/SerializableLambdaReaderBenchmarks.java b/src/jmh/java/org/springframework/data/core/SerializableLambdaReaderBenchmarks.java
new file mode 100644
index 0000000000..04d6e0660f
--- /dev/null
+++ b/src/jmh/java/org/springframework/data/core/SerializableLambdaReaderBenchmarks.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+package org.springframework.data.core;
+
+import org.junit.platform.commons.annotation.Testable;
+import org.openjdk.jmh.annotations.Benchmark;
+
+import org.springframework.data.BenchmarkSettings;
+
+/**
+ * Benchmarks for {@link SerializableLambdaReader}.
+ *
+ * @author Mark Paluch
+ */
+@Testable
+public class SerializableLambdaReaderBenchmarks extends BenchmarkSettings {
+
+ private static final SerializableLambdaReader reader = new SerializableLambdaReader(PropertyReference.class);
+
+ @Benchmark
+ public Object benchmarkMethodReference() {
+
+ PropertyReference methodReference = Person::firstName;
+ return reader.read(methodReference);
+ }
+
+ @Benchmark
+ public Object benchmarkLambda() {
+
+ PropertyReference methodReference = person -> person.firstName();
+ return reader.read(methodReference);
+ }
+
+ record Person(String firstName, String lastName) {
+
+ }
+
+}
diff --git a/src/jmh/java/org/springframework/data/core/TypedPropertyPathBenchmarks.java b/src/jmh/java/org/springframework/data/core/TypedPropertyPathBenchmarks.java
new file mode 100644
index 0000000000..245641dc03
--- /dev/null
+++ b/src/jmh/java/org/springframework/data/core/TypedPropertyPathBenchmarks.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+package org.springframework.data.core;
+
+import org.junit.platform.commons.annotation.Testable;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.springframework.data.BenchmarkSettings;
+
+/**
+ * Benchmarks for {@link TypedPropertyPath}.
+ *
+ * @author Mark Paluch
+ */
+@Testable
+public class TypedPropertyPathBenchmarks extends BenchmarkSettings {
+
+ @Benchmark
+ public Object benchmarkMethodReference() {
+ return TypedPropertyPath.path(Person::firstName);
+ }
+
+ @Benchmark
+ public Object benchmarkComposedMethodReference() {
+ return TypedPropertyPath.path(Person::address).then(Address::city);
+ }
+
+ @Benchmark
+ public TypedPropertyPath benchmarkLambda() {
+ return TypedPropertyPath.path(person -> person.firstName());
+ }
+
+ @Benchmark
+ public TypedPropertyPath benchmarkComposedLambda() {
+ return TypedPropertyPath.path((Person person) -> person.address()).then(address -> address.city());
+ }
+
+ @Benchmark
+ public Object dotPath() {
+ return TypedPropertyPath.path(Person::firstName).toDotPath();
+ }
+
+ @Benchmark
+ public Object composedDotPath() {
+ return TypedPropertyPath.path(Person::address).then(Address::city).toDotPath();
+ }
+
+ record Person(String firstName, String lastName, Address address) {
+
+ }
+
+ record Address(String city) {
+
+ }
+
+}
diff --git a/src/main/antora/modules/ROOT/pages/property-paths.adoc b/src/main/antora/modules/ROOT/pages/property-paths.adoc
new file mode 100644
index 0000000000..d1dc0982ed
--- /dev/null
+++ b/src/main/antora/modules/ROOT/pages/property-paths.adoc
@@ -0,0 +1,204 @@
+[[property-paths]]
+= Property Paths
+
+This chapter covers the concept of property paths.
+Property paths are a form of navigation through domain classes to apply certain aspects in the context of interacting with the model.
+Application code provides property paths to data access components to express intents such as selection of properties within a query, forming predicates, or applying sorting.
+A property path originates from its owning type and can consist of one to many segments.
+
+[TIP]
+====
+Following domain-driven design principles the classes that form the backbone of your persistent domain model and that are accessed through Spring Data are called entities.
+An entry point to the object graph is called aggregate root.
+
+Understanding how to navigate and reference these properties is essential for working with repositories and query operations.
+====
+
+[[property-path-overview]]
+== Property Path Overview
+
+Property paths provide a simple, text-based mechanism to navigate domain model properties.
+This section introduces the fundamentals of property path navigation and demonstrates trade-offs between string-based and type-safe approaches.
+
+.Domain model example
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+class Person {
+ String firstname, lastname;
+ int age;
+ Address address;
+ List previousAddresses;
+
+ String getFirstname() { … } // other property accessors omitted for brevity
+
+}
+
+class Address {
+ String city, street;
+
+ // accessors omitted for brevity
+
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+class Person {
+ var firstname: String? = null
+ var lastname: String? = null
+ var age: Int = 0
+ var address: Address? = null
+ var previousAddresses: List = emptyList()
+}
+
+class Address {
+ var city: String? = null
+ var street: String? = null
+}
+----
+======
+
+Property paths use dot-notation to express property references throughout Spring Data operations, such as sorting and filtering:
+
+.Dot-notation property references
+[source,java]
+----
+Sort.by("firstname", "address.city")
+----
+
+A property path consists of one or more segments separated by a dot (`.`).
+Methods accepting property paths support single-segment references (top-level properties) and multi-segment navigation unless otherwise indicated.
+
+Collection and array properties support transparent traversal to their component type, enabling direct reference to nested properties:
+
+----
+Sort.by("address.city") <1>
+
+Sort.by("previousAddresses") <2>
+
+Sort.by("previousAddresses.city") <3>
+----
+
+<1> Navigate from the top-level `address` property to the `city` field.
+<2> Reference the entire `previousAddresses` collection (supported by certain technologies for collection-based sorting).
+<3> Navigate through the collection to sort by the `city` field of each address.
+
+String-based property paths offer simplicity and can be broadly applied but there are tradeoffs to consider:
+
+* **Flexibility**: Property paths are flexible and can be constructed from constant string, configuration or as result of user input.
+* **Untyped**: String paths do not carry compile-time type information.
+Typed as textual content they do not have a dependency on the underlying domain type.
+* **Refactoring risk**: Renaming domain properties requires often manual updates to string literals; IDEs cannot reliably track these references.
+
+To improve refactoring safety and type consistency, prefer type-safe property references using method references.
+This approach associates property paths with compile-time type information and enables compiler validation and IDE-driven refactoring.
+See <> for details.
+
+NOTE: For implementation details, refer to <> for more information.
+
+[[property-path-internals]]
+=== Property Path Internals
+
+The `org.springframework.data.core` package is the basis for Spring Data's navigation across domain classes.
+The javadoc:org.springframework.data.core.TypeInformation[] inteface provides type introspection capable of resolving the type of a property. javadoc:org.springframework.data.core.PropertyPath[] represents a textual navigation path through a domain class.
+
+Together they provide:
+
+* Generic type resolution and introspection
+* Property path creation and validation
+* Actual type resolution for complex properties such as collections and maps
+
+[[type-safe-property-references]]
+== Type-safe Property-References
+
+Type-safe property-references eliminate a common source of errors in data access code: Brittle, string-based property references.
+This section explains how method references can be used to express refactoring-safe property paths.
+
+While a property path is a simple representation of object navigation, String-based property paths are inherently fragile during refactoring as they can be easily missed with an increasing distance between the property definition and its usage.
+Type-safe alternatives derive property paths from method references, enabling the compiler to validate property names and IDEs to support refactoring operations.
+
+Consider the practical difference: The following examples express the same intent - sorting by `address.city` - but only the type-safe version benefits from compiler validation and IDE support:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+// Static import variant
+path(Person::getAddress)
+ .then(Address::getCity);
+
+// Composition factory method
+path(Person::getAddress, Address::getCity);
+
+// Fluent composition
+TypedPropertyPath.of(Person::getAddress)
+ .then(Address::getCity);
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+// Kotlin API
+TypedPropertyPath.of(Person::address / Address::city)
+
+// as extension function
+(Person::address / Address::city).toPath()
+----
+======
+
+=== Building Type-safe Property Paths
+
+Type-safe property paths compose method references to construct navigation expressions.
+The following examples demonstrate inline usage and composition:
+
+.Type-safe Property Path Construction
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+// Inline usage with Sort
+Sort.by(Person::getFirstName, Person::getLastName);
+
+// Composed navigation
+Sort.by(TypedPropertyPath.of(Person::getAddress).then(Address::getCity),
+ Person::getLastName);
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+// Inline usage with Sort
+Sort.by(Person::firstName, Person::lastName)
+
+// Composed navigation
+Sort.by(Person::address / Address::city, Person::lastName)
+----
+======
+
+Type-safe property paths integrate seamlessly with query abstractions and criteria builders, enabling declarative query construction without string-based property references:
+
+.Integration with Criteria API
+[source,java]
+----
+Criteria.where(Person::getAddress)
+ .then(Address::getCity)
+ .is("New York");
+----
+
+Adopting type-safe property references aligns with modern Spring development principles.
+Providing declarative, type-safe, and fluent APIs leads to simpler reasoning about data access eliminating an entire category of potential bugs through IDE refactoring support and early feedback on invalid properties by the compiler.
+
+Lambda introspection is cached for efficiency enabling repeatable use.
+The JVM reuses static lambda instances contributing to minimal overhead of one-time parsing.
diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java b/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java
index 869b60269e..1606dc7fed 100644
--- a/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java
+++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java
@@ -20,6 +20,7 @@
import java.util.function.Function;
import org.springframework.data.convert.PropertyValueConverter.FunctionPropertyValueConverter;
+import org.springframework.data.core.PropertyReference;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.util.MethodInvocationRecorder;
import org.springframework.lang.Contract;
@@ -50,11 +51,25 @@ public class PropertyValueConverterRegistrar
> {
*
* @param the domain type
* @param the property type
- * @param type the domain type to obtain the property from
* @param property a {@link Function} to describe the property to be referenced.
* Usually a method handle to a getter.
* @return will never be {@literal null}.
*/
+ public WritingConverterRegistrationBuilder registerConverter(PropertyReference property) {
+ return new WritingConverterRegistrationBuilder<>(property.getOwningType().getType(), property.getName(), this);
+ }
+
+ /**
+ * Starts a converter registration by pointing to a property of a domain type.
+ *
+ * @param the domain type
+ * @param the property type
+ * @param type the domain type to obtain the property from
+ * @param property a {@link Function} to describe the property to be referenced. Usually a method handle to a getter.
+ * @return will never be {@literal null}.
+ * @deprecated since 4.1, use {@link #registerConverter(PropertyReference)} instead.
+ */
+ @Deprecated(since = "4.1")
public WritingConverterRegistrationBuilder registerConverter(Class type, Function property) {
String propertyName = MethodInvocationRecorder.forProxyOf(type).record(property).getPropertyPath()
diff --git a/src/main/java/org/springframework/data/core/MemberDescriptor.java b/src/main/java/org/springframework/data/core/MemberDescriptor.java
new file mode 100644
index 0000000000..4ce02107f5
--- /dev/null
+++ b/src/main/java/org/springframework/data/core/MemberDescriptor.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+package org.springframework.data.core;
+
+import kotlin.reflect.KProperty1;
+import kotlin.reflect.jvm.ReflectJvmMapping;
+
+import java.lang.invoke.SerializedLambda;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+
+import org.springframework.asm.Type;
+import org.springframework.core.ResolvableType;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Interface representing a member reference such as a field or method.
+ *
+ * @author Mark Paluch
+ * @since 4.1
+ */
+interface MemberDescriptor {
+
+ /**
+ * @return class owning the member, can be the declaring class or a subclass.
+ */
+ Class> getOwner();
+
+ /**
+ * @return the member (field or method).
+ */
+ Member getMember();
+
+ /**
+ * @return field type or method return type.
+ */
+ ResolvableType getType();
+
+ /**
+ * Create {@link MethodDescriptor} from a serialized lambda representing a method reference.
+ */
+ static MethodDescriptor ofMethodReference(ClassLoader classLoader, SerializedLambda lambda)
+ throws ClassNotFoundException {
+ return ofMethod(classLoader, Type.getObjectType(lambda.getImplClass()).getClassName(), lambda.getImplMethodName());
+ }
+
+ /**
+ * Create {@link MethodDescriptor} from owner type and method name.
+ */
+ static MethodDescriptor ofMethod(ClassLoader classLoader, String ownerClassName, String name)
+ throws ClassNotFoundException {
+ Class> owner = ClassUtils.forName(ownerClassName, classLoader);
+ return MethodDescriptor.create(owner, name);
+ }
+
+ /**
+ * Create {@link MethodDescriptor.FieldDescriptor} from owner type, field name and field type.
+ */
+ static MethodDescriptor.FieldDescriptor ofField(ClassLoader classLoader, String ownerClassName, String name,
+ String fieldType) throws ClassNotFoundException {
+
+ Class> owner = ClassUtils.forName(ownerClassName, classLoader);
+ Class> type = ClassUtils.forName(fieldType, classLoader);
+
+ return FieldDescriptor.create(owner, name, type);
+ }
+
+ /**
+ * Value object describing a {@link Method} in the context of an owning class.
+ */
+ record MethodDescriptor(Class> owner, Method method) implements MemberDescriptor {
+
+ static MethodDescriptor create(Class> owner, String methodName) {
+ Method method = ReflectionUtils.findMethod(owner, methodName);
+ if (method == null) {
+ throw new IllegalArgumentException("Method '%s.%s()' not found".formatted(owner.getName(), methodName));
+ }
+ return new MethodDescriptor(owner, method);
+ }
+
+ @Override
+ public Class> getOwner() {
+ return owner();
+ }
+
+ @Override
+ public Method getMember() {
+ return method();
+ }
+
+ @Override
+ public ResolvableType getType() {
+ return ResolvableType.forMethodReturnType(method(), owner());
+ }
+
+ }
+
+ /**
+ * Value object describing a {@link Field} in the context of an owning class.
+ */
+ record FieldDescriptor(Class> owner, Field field) implements MemberDescriptor {
+
+ static FieldDescriptor create(Class> owner, String fieldName, Class> fieldType) {
+
+ Field field = ReflectionUtils.findField(owner, fieldName, fieldType);
+ if (field == null) {
+ throw new IllegalArgumentException("Field '%s.%s' not found".formatted(owner.getName(), fieldName));
+ }
+ return new FieldDescriptor(owner, field);
+ }
+
+ @Override
+ public Class> getOwner() {
+ return owner();
+ }
+
+ @Override
+ public Field getMember() {
+ return field();
+ }
+
+ @Override
+ public ResolvableType getType() {
+ return ResolvableType.forField(field(), owner());
+ }
+
+ }
+
+ interface KotlinMemberDescriptor extends MemberDescriptor {
+
+ KProperty1, ?> getKotlinProperty();
+
+ }
+
+ /**
+ * Value object describing a Kotlin property in the context of an owning class.
+ */
+ record KPropertyReferenceDescriptor(Class> owner, KProperty1, ?> property) implements KotlinMemberDescriptor {
+
+ static KPropertyReferenceDescriptor create(Class> owner, KProperty1, ?> property) {
+ return new KPropertyReferenceDescriptor(owner, property);
+ }
+
+ @Override
+ public KProperty1, ?> getKotlinProperty() {
+ return property();
+ }
+
+ @Override
+ public Class> getOwner() {
+ return owner();
+ }
+
+ @Override
+ public Member getMember() {
+
+ Method javaGetter = ReflectJvmMapping.getJavaGetter(property());
+ if (javaGetter != null) {
+ return javaGetter;
+ }
+
+ Field javaField = ReflectJvmMapping.getJavaField(property());
+
+ if (javaField != null) {
+ return javaField;
+ }
+
+ throw new IllegalStateException("Cannot resolve member for property '%s'".formatted(property().getName()));
+ }
+
+ @Override
+ public ResolvableType getType() {
+
+ Member member = getMember();
+
+ if (member instanceof Method m) {
+ return ResolvableType.forMethodReturnType(m, owner());
+ }
+
+ return ResolvableType.forField((Field) member, owner());
+ }
+
+ }
+
+ /**
+ * Value object describing a Kotlin property in the context of an owning class.
+ */
+ record KPropertyPathDescriptor(KProperty1, ?> property) implements KotlinMemberDescriptor {
+
+ static KPropertyPathDescriptor create(KProperty1, ?> propertyReference) {
+ return new KPropertyPathDescriptor(propertyReference);
+ }
+
+ @Override
+ public KProperty1, ?> getKotlinProperty() {
+ return property();
+ }
+
+ @Override
+ public Class> getOwner() {
+ return getMember().getDeclaringClass();
+ }
+
+ @Override
+ public Member getMember() {
+
+ Method javaGetter = ReflectJvmMapping.getJavaGetter(property());
+ if (javaGetter != null) {
+ return javaGetter;
+ }
+
+ Field javaField = ReflectJvmMapping.getJavaField(property());
+
+ if (javaField != null) {
+ return javaField;
+ }
+
+ throw new IllegalStateException("Cannot resolve member for property '%s'".formatted(property().getName()));
+ }
+
+ @Override
+ public ResolvableType getType() {
+
+ Member member = getMember();
+
+ if (member instanceof Method m) {
+ return ResolvableType.forMethodReturnType(m, getOwner());
+ }
+
+ return ResolvableType.forField((Field) member, getOwner());
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/core/PropertyPath.java b/src/main/java/org/springframework/data/core/PropertyPath.java
index 26a06299c9..68b7235e01 100644
--- a/src/main/java/org/springframework/data/core/PropertyPath.java
+++ b/src/main/java/org/springframework/data/core/PropertyPath.java
@@ -19,21 +19,67 @@
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
-
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
/**
* Abstraction of a {@link PropertyPath} within a domain class.
+ *
+ * Property paths allow to navigate nested properties such as {@code address.city.name} and provide metadata for each
+ * segment of the path. Paths are represented in dot-path notation and are resolved from an owning type, for example:
+ *
+ *
+ *
+ * Paths are cached on a best-effort basis using a weak reference cache to avoid repeated introspection if GC pressure
+ * permits.
+ *
+ * A typed variant of {@link PropertyPath} is available as {@link TypedPropertyPath} through
+ * {@link #of(PropertyReference)} to leverage method references for a type-safe usage across application code.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @author Mariusz Mączkowski
* @author Johannes Englmeier
+ * @see PropertyReference
+ * @see TypedPropertyPath
+ * @see java.beans.PropertyDescriptor
*/
public interface PropertyPath extends Streamable {
+ /**
+ * Syntax sugar to create a {@link TypedPropertyPath} from a method reference to a Java beans property.
+ *
+ * This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference.
+ *
+ * @param property the method reference referring to a Java beans property.
+ * @param owning type.
+ * @param
property type.
+ * @return the typed property path.
+ * @since 4.1
+ */
+ static TypedPropertyPath of(PropertyReference property) {
+ return TypedPropertyPaths.of(property);
+ }
+
+ /**
+ * Syntax sugar to create a {@link TypedPropertyPath} from a method reference to a Java beans collection property.
+ *
+ * This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference.
+ *
+ * @param property the method reference referring to a property.
+ * @param owning type.
+ * @param
property type.
+ * @return the typed property path.
+ * @since 4.1
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ static TypedPropertyPath ofMany(PropertyReference> property) {
+ return (TypedPropertyPath) TypedPropertyPaths.of(property);
+ }
+
/**
* Returns the owning type of the {@link PropertyPath}.
*
@@ -42,22 +88,25 @@ public interface PropertyPath extends Streamable {
TypeInformation> getOwningType();
/**
- * Returns the first part of the {@link PropertyPath}. For example:
+ * Returns the current property path segment (i.e. first part of {@link #toDotPath()}).
+ *
*
- * results in {@code a}.
+ * results in {@code address}.
*
- * @return the name will never be {@literal null}.
+ * @return the current property path segment.
*/
String getSegment();
/**
- * Returns the leaf property of the {@link PropertyPath}.
+ * Returns the leaf property of the {@link PropertyPath}. Either this property if the path ends here or the last
+ * property in the chain.
*
- * @return will never be {@literal null}.
+ * @return leaf property.
*/
default PropertyPath getLeafProperty() {
@@ -74,57 +123,72 @@ default PropertyPath getLeafProperty() {
* Returns the type of the leaf property of the current {@link PropertyPath}.
*
* @return will never be {@literal null}.
+ * @see #getLeafProperty()
*/
default Class> getLeafType() {
return getLeafProperty().getType();
}
/**
- * Returns the actual type of the property. Will return the plain resolved type for simple properties, the component
- * type for any {@link Iterable} or the value type of a {@link java.util.Map}.
+ * Returns the actual type of the property at this segment. Will return the plain resolved type for simple properties,
+ * the component type for any {@link Iterable} or the value type of {@link java.util.Map} properties.
*
* @return the actual type of the property.
+ * @see #getTypeInformation()
+ * @see TypeInformation#getRequiredActualType()
*/
default Class> getType() {
return getTypeInformation().getRequiredActualType().getType();
}
/**
- * Returns the type information of the property.
+ * Returns the type information for the property at this segment.
*
- * @return the actual type of the property.
+ * @return the type information for the property at this segment.
*/
TypeInformation> getTypeInformation();
/**
- * Returns the {@link PropertyPath} path that results from removing the first element of the current one. For example:
+ * Returns whether the current property path segment is a collection.
+ *
+ * @return {@literal true} if the current property path segment is a collection.
+ * @see #getTypeInformation()
+ * @see TypeInformation#isCollectionLike()
+ */
+ default boolean isCollection() {
+ return getTypeInformation().isCollectionLike();
+ }
+
+ /**
+ * Returns the next {@code PropertyPath} segment in the property path chain.
*
*
*
- * results in the output: {@code b.c}
+ * results in the output: {@code city.name}.
*
- * @return the next nested {@link PropertyPath} or {@literal null} if no nested {@link PropertyPath} available.
+ * @return the next {@code PropertyPath} or {@literal null} if the path does not contain further segments.
* @see #hasNext()
*/
@Nullable
PropertyPath next();
/**
- * Returns whether there is a nested {@link PropertyPath}. If this returns {@literal true} you can expect
- * {@link #next()} to return a non- {@literal null} value.
+ * Returns {@literal true} if the property path contains further segments or {@literal false} if the path ends at this
+ * segment.
*
- * @return
+ * @return {@literal true} if the property path contains further segments or {@literal false} if the path ends at this
+ * segment.
*/
default boolean hasNext() {
return next() != null;
}
/**
- * Returns the {@link PropertyPath} in dot notation.
+ * Returns the {@code PropertyPath} in dot notation.
*
- * @return the {@link PropertyPath} in dot notation.
+ * @return the {@code PropertyPath} in dot notation.
*/
default String toDotPath() {
@@ -133,16 +197,7 @@ default String toDotPath() {
}
/**
- * Returns whether the {@link PropertyPath} is actually a collection.
- *
- * @return {@literal true} whether the {@link PropertyPath} is actually a collection.
- */
- default boolean isCollection() {
- return getTypeInformation().isCollectionLike();
- }
-
- /**
- * Returns the {@link PropertyPath} for the path nested under the current property.
+ * Returns the {@code PropertyPath} for the path nested under the current property.
*
* @param path must not be {@literal null} or empty.
* @return will never be {@literal null}.
@@ -157,20 +212,19 @@ default PropertyPath nested(String path) {
}
/**
- * Returns an {@link Iterator Iterator of PropertyPath} that iterates over all the partial property paths with the
- * same leaf type but decreasing length. For example:
+ * Returns an {@link Iterator Iterator of PropertyPath} that iterates over all property path segments. For example:
*
*
*/
@Override
diff --git a/src/main/java/org/springframework/data/core/PropertyPathUtil.java b/src/main/java/org/springframework/data/core/PropertyPathUtil.java
new file mode 100644
index 0000000000..b46c47d84e
--- /dev/null
+++ b/src/main/java/org/springframework/data/core/PropertyPathUtil.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+package org.springframework.data.core;
+
+import java.io.Serializable;
+import java.lang.invoke.SerializedLambda;
+import java.lang.reflect.Method;
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Utility class for {@link PropertyPath} and {@link PropertyReference} implementations.
+ *
+ * @author Mark Paluch
+ * @since 4.1
+ */
+public class PropertyPathUtil {
+
+ /**
+ * Resolve a {@link PropertyPath} from a {@link Serializable} lambda implementing a functional interface accepting a
+ * single method argument and returning a value. The form of the interface must follow a design aligned with
+ * {@link org.springframework.core.convert.converter.Converter} or {@link java.util.function.Function}.
+ *
+ * @param obj the serializable lambda object.
+ * @return the resolved property path.
+ */
+ public static PropertyPath resolve(Object obj) {
+
+ Assert.isInstanceOf(Serializable.class, obj, "Object must be Serializable");
+
+ return TypedPropertyPaths.of(new SerializableWrapper((Serializable) obj));
+ }
+
+ private record SerializableWrapper(Serializable serializable) implements PropertyReference