diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3926eb67..57bbdf76 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,13 +7,13 @@ arquillian-container = "1.9.1.Final"
javax-annotation-api = "1.3.2"
jakarta-validation-tck = "3.0.1"
-micronaut = "4.6.6"
-micronaut-platform = "4.4.3"
+micronaut = "4.7.0"
+micronaut-platform = "4.6.3"
micronaut-docs = "2.0.0"
micronaut-test = "4.5.0"
-micronaut-reactor = "3.4.1"
-micronaut-rxjava2 = "2.4.0"
-micronaut-kotlin = "4.3.0"
+micronaut-reactor = "3.5.0"
+micronaut-rxjava2 = "2.5.0"
+micronaut-kotlin = "4.4.0"
micronaut-logging = "1.3.0"
# Gradle plugins
diff --git a/tests/jakarta-validation-tck/tck-tests.xml b/tests/jakarta-validation-tck/tck-tests.xml
index d19397af..42266ad8 100644
--- a/tests/jakarta-validation-tck/tck-tests.xml
+++ b/tests/jakarta-validation-tck/tck-tests.xml
@@ -368,6 +368,13 @@
+
+
+
+
+
+
+
diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java
index 942d04bf..ff7977d8 100644
--- a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java
+++ b/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java
@@ -23,6 +23,7 @@
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
+import io.micronaut.inject.BeanDefinition;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.validation.valueextraction.ValueExtractor;
@@ -78,10 +79,23 @@ protected DefaultValueExtractors(@Nullable BeanContext beanContext) {
final Collection> valueExtractors = beanContext.getBeanRegistrations(ValueExtractor.class);
if (CollectionUtils.isNotEmpty(valueExtractors)) {
for (BeanRegistration reg : valueExtractors) {
- addValueExtractor(localValueExtractors, new ValueExtractorDefinition(
- reg.getBeanDefinition().asArgument(),
- reg.getBean()
- ));
+ BeanDefinition beanDefinition = reg.getBeanDefinition();
+ Argument argument = beanDefinition.asArgument();
+ if (argument.getType().equals(ValueExtractor.class)) {
+ addValueExtractor(localValueExtractors, new ValueExtractorDefinition(
+ argument,
+ reg.getBean()
+ ));
+ } else {
+ List> typeArguments = beanDefinition.getTypeArguments(ValueExtractor.class);
+ if (typeArguments.isEmpty()) {
+ throw new IllegalStateException("No value-extractors found for bean definition: " + beanDefinition);
+ }
+ addValueExtractor(localValueExtractors, new ValueExtractorDefinition(
+ Argument.of(ValueExtractor.class, beanDefinition.getAnnotationMetadata(), typeArguments.toArray(new Argument[0])),
+ reg.getBean()
+ ));
+ }
}
}
}
diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java
index 296f4622..0d78956a 100644
--- a/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java
+++ b/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java
@@ -82,6 +82,10 @@ private static Integer findExtractedTypeArgumentIndex(@NotNull Argument> argum
if (typeArgumentIndex != null) {
return typeArgumentIndex;
}
+ if (typeParameters.length == 1) {
+ // On missing @ExtractedValue select first type parameter by default
+ return 0;
+ }
throw new ValueExtractorDefinitionException("ValueExtractor definition is missing @ExtractedValue on an argument: " + argument);
}
@@ -96,6 +100,10 @@ private static AnnotationValue> findExtractedValue(@NotNull Argument> argume
}
AnnotationValue annotationValue = argument.getAnnotationMetadata().getAnnotation(ExtractedValue.class);
if (annotationValue == null) {
+ if (typeParameters.length == 1) {
+ // On missing @ExtractedValue select first type parameter by default
+ return AnnotationValue.builder(ExtractedValue.class).build();
+ }
throw new ValueExtractorDefinitionException("ValueExtractor definition '" + valueExtractor + "' is missing @ExtractedValue!");
}
if (annotationValue.classValue("type").isEmpty()) {
diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy
index 6f2674d1..1a6ab096 100644
--- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy
+++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy
@@ -1,18 +1,12 @@
package io.micronaut.validation.validator.constraints
-import io.micronaut.annotation.processing.TypeElementVisitorProcessor
+
import io.micronaut.annotation.processing.test.AbstractTypeElementSpec
import io.micronaut.context.ApplicationContext
-import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor
-import io.micronaut.inject.visitor.TypeElementVisitor
import io.micronaut.validation.validator.Validator
-import io.micronaut.validation.visitor.IntrospectedValidationIndexesVisitor
-import io.micronaut.validation.visitor.ValidationVisitor
import spock.lang.AutoCleanup
import spock.lang.Shared
-import javax.annotation.processing.SupportedAnnotationTypes
-
class ConstraintUnwrapSpec extends AbstractTypeElementSpec {
@Shared
@@ -259,11 +253,4 @@ class Test {
constraintViolations.iterator().next().message == "must not be null"
}
- @SupportedAnnotationTypes("*")
- static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor {
- @Override
- protected Collection findTypeElementVisitors() {
- return [new ValidationVisitor(), new IntrospectedValidationIndexesVisitor(), new IntrospectedTypeElementVisitor()]
- }
- }
}
diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptional.java b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptional.java
new file mode 100644
index 00000000..a9c18713
--- /dev/null
+++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptional.java
@@ -0,0 +1,4 @@
+package io.micronaut.validation.validator.constraints.unwrapped;
+
+public record MyOptional(V value) {
+}
diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptionalExtractor.java b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptionalExtractor.java
new file mode 100644
index 00000000..2b12b929
--- /dev/null
+++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptionalExtractor.java
@@ -0,0 +1,16 @@
+package io.micronaut.validation.validator.constraints.unwrapped;
+
+import jakarta.inject.Singleton;
+import jakarta.validation.valueextraction.UnwrapByDefault;
+import jakarta.validation.valueextraction.ValueExtractor;
+
+@UnwrapByDefault
+@Singleton
+public class MyOptionalExtractor implements ValueExtractor> {
+
+ @Override
+ public void extractValues(MyOptional> originalValue, ValueReceiver receiver) {
+ receiver.value("value", originalValue.value());
+ }
+
+}
diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/ValueExtractorsSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/ValueExtractorsSpec.groovy
new file mode 100644
index 00000000..455e3ba5
--- /dev/null
+++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/ValueExtractorsSpec.groovy
@@ -0,0 +1,51 @@
+package io.micronaut.validation.validator.constraints.unwrapped
+
+
+import io.micronaut.annotation.processing.test.AbstractTypeElementSpec
+import io.micronaut.context.ApplicationContext
+import io.micronaut.validation.validator.Validator
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+
+class ValueExtractorsSpec extends AbstractTypeElementSpec {
+
+ @Shared
+ @AutoCleanup
+ ApplicationContext context = ApplicationContext.run()
+
+ @Shared
+ Validator validator = context.getBean(Validator)
+
+ void "test @NotNull should be applied on the optional value"() {
+ given:
+ def introspection = buildBeanIntrospection('test.Test', """
+package test;
+
+import jakarta.validation.Payload;
+import jakarta.validation.constraints.NotNull;
+import io.micronaut.validation.validator.constraints.unwrapped.MyOptional;
+
+@io.micronaut.core.annotation.Introspected
+class Test {
+ @NotNull
+ private MyOptional field;
+
+ public MyOptional getField() {
+ return field;
+ }
+
+ public void setField(MyOptional f) {
+ this.field = f;
+ }
+}
+""")
+ def instance = introspection.instantiate()
+ def prop = introspection.getProperty("field").get()
+ prop.set(instance, new MyOptional(null))
+ def constraintViolations = validator.validate(introspection, instance)
+
+ expect:
+ constraintViolations.size() == 1
+ constraintViolations.iterator().next().message == "must not be null"
+ }
+}