Repository: swagger-api/swagger-core (language: java)

Issue:
Support Bean Validation for generic type arguments
Original issue is [here](https://github.com/springdoc/springdoc-openapi/issues/2042) and I've been redirected to report it here.

https://github.com/springdoc/springdoc-openapi/issues/2042#issuecomment-1417665098

> @lpandzic,
This logic is handled in the swagger-core library.
Please submit this Enhancement in the correct Github 
https://github.com/swagger-api/swagger-core

The original issue is as follows:

Given the controller:

```java
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Value;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;

@RestController
public class FooController {

    @PostMapping("fooRecord")
    public void fooRecord(@RequestBody FooRecord foo) {

    }

    @PostMapping("fooClass")
    public void fooClass(@RequestBody FooClass foo) {

    }

    record FooRecord(@NotNull Optional<@Size(max = 1000) String> value) {

    }

    @Value
    static class FooClass {
        @NotNull
        @Size(max = 1000)
        String value;

        public Optional<String> getValue() {
            return Optional.of(value);
        }
    }
}
```

currently with latest springdoc generated openapi looks like:

```json
      "FooRecord":{
        "required":[
          "value"
        ],
        "type":"object",
        "properties":{
          "value":{
            "type":"string"
          }
        }
      },
      "FooClass":{
        "required":[
          "value"
        ],
        "type":"object",
        "properties":{
          "value":{
            "maxLength":1000,
            "minLength":0,
            "type":"string"
          }
        }
      },
```
under components schemas.
The problematic part is the FooRecord component.

The result I'd expect in ideal case would be


```json
      "FooRecord":{
        "type":"object",
        "properties":{
          "value":{
            "maxLength":1000,
            "minLength":0,
            "type":"string"
          }
        }
      },
```

EDIT: I've assumed this doesn't work only for Optional types, but after implementing it turns out it doesn't work at all for generic type arguments on any type.


Discussion / hints:

Fix patch:
diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java
index cfdcf8cbd8..bb2905d941 100644
--- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java
+++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java
@@ -21,6 +21,7 @@
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.fasterxml.jackson.databind.introspect.Annotated;
 import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
+import com.fasterxml.jackson.databind.introspect.AnnotatedField;
 import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
 import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
 import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
@@ -77,6 +78,7 @@
 import javax.xml.bind.annotation.XmlSchema;
 import java.io.IOException;
 import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedParameterizedType;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -94,6 +96,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.Objects;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -703,7 +706,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
                         addRequiredItem(model, property.getName());
                     }
                     final boolean applyNotNullAnnotations = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.AUTO.equals(requiredMode);
-                    applyBeanValidatorAnnotations(property, annotations, model, applyNotNullAnnotations);
+                    annotations = addGenericTypeArgumentAnnotationsForOptionalField(propDef, annotations);
+                    applyBeanValidatorAnnotations(propDef, property, annotations, model, applyNotNullAnnotations);
 
                     props.add(property);
                 }
@@ -905,6 +909,40 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
         return model;
     }
 
+    private Annotation[] addGenericTypeArgumentAnnotationsForOptionalField(BeanPropertyDefinition propDef, Annotation[] annotations) {
+
+        boolean isNotOptionalType = Optional.ofNullable(propDef)
+                                            .map(BeanPropertyDefinition::getField)
+                                            .map(AnnotatedField::getAnnotated)
+                                            .map(field -> !(field.getType().equals(Optional.class)))
+                                            .orElse(false);
+
+        if (isNotOptionalType) {
+            return annotations;
+        }
+
+        Stream<Annotation> genericTypeArgumentAnnotations = extractGenericTypeArgumentAnnotations(propDef);
+        return Stream.concat(Stream.of(annotations), genericTypeArgumentAnnotations).toArray(Annotation[]::new);
+    }
+
+    private Stream<Annotation> extractGenericTypeArgumentAnnotations(BeanPropertyDefinition propDef) {
+        return Optional.ofNullable(propDef)
+                       .map(BeanPropertyDefinition::getField)
+                       .map(AnnotatedField::getAnnotated)
+                       .map(this::getGenericTypeArgumentAnnotations)
+                       .orElseGet(Stream::of);
+    }
+
+    private Stream<Annotation> getGenericTypeArgumentAnnotations(Field field) {
+        return Optional.of(field.getAnnotatedType())
+                       .filter(annotatedType -> annotatedType instanceof AnnotatedParameterizedType)
+                       .map(annotatedType -> (AnnotatedParameterizedType) annotatedType)
+                       .map(AnnotatedParameterizedType::getAnnotatedActualTypeArguments)
+                       .map(types -> Stream.of(types)
+                                           .flatMap(type -> Stream.of(type.getAnnotations())))
+                       .orElseGet(Stream::of);
+    }
+
     private boolean shouldResolveEnumAsRef(io.swagger.v3.oas.annotations.media.Schema resolvedSchemaAnnotation) {
         return (resolvedSchemaAnnotation != null && resolvedSchemaAnnotation.enumAsRef()) || ModelResolver.enumsAsRef;
     }
@@ -1298,6 +1336,15 @@ private AnnotatedType removeJsonIdentityAnnotations(AnnotatedType type) {
         }
     }
 
+    protected void applyBeanValidatorAnnotations(BeanPropertyDefinition propDef, Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
+        applyBeanValidatorAnnotations(property, annotations, parent, applyNotNullAnnotations);
+
+        if (Objects.nonNull(property.getItems())) {
+            Annotation[] genericTypeArgumentAnnotations = extractGenericTypeArgumentAnnotations(propDef).toArray(Annotation[]::new);
+            applyBeanValidatorAnnotations(property.getItems(), genericTypeArgumentAnnotations, property, applyNotNullAnnotations);
+        }
+    }
+
     protected void applyBeanValidatorAnnotations(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
         Map<String, Annotation> annos = new HashMap<>();
         if (annotations != null) {


Test patch:
diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java
index 11156c0793..ce75707817 100644
--- a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java
+++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java
@@ -8,6 +8,7 @@
 import javax.validation.constraints.Pattern;
 import javax.validation.constraints.Size;
 import java.util.List;
+import java.util.Optional;
 
 public class BeanValidationsModel {
     @NotNull
@@ -36,7 +37,9 @@ public class BeanValidationsModel {
     protected Integer birthYear;
 
     @Size(min = 2, max = 10)
-    private List<String> items;
+    private List<@Size(min = 3, max = 4) String> items;
+
+    private Optional<@Size(min= 1, max=10) String> optionalValue;
 
     public Long getId() {
         return id;
@@ -117,4 +120,13 @@ public List<String> getItems() {
     public void setItems(List<String> items) {
         this.items = items;
     }
+
+    public Optional<String> getOptionalValue() {
+        return optionalValue;
+    }
+
+    public void setOptionalValue(Optional<String> optionalValue) {
+        this.optionalValue = optionalValue;
+    }
+
 }
diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java
index 6388696241..9a00b4b6ef 100644
--- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java
+++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java
@@ -43,5 +43,11 @@ public void readBeanValidatorTest() {
         final ArraySchema items = (ArraySchema) properties.get("items");
         Assert.assertEquals((int) items.getMinItems(), 2);
         Assert.assertEquals((int) items.getMaxItems(), 10);
+        Assert.assertEquals((int) items.getItems().getMinLength(), 3);
+        Assert.assertEquals((int) items.getItems().getMaxLength(), 4);
+
+        final StringSchema optionalValue = (StringSchema) properties.get("optionalValue");
+        Assert.assertEquals((int) optionalValue.getMinLength(), 1);
+        Assert.assertEquals((int) optionalValue.getMaxLength(), 10);
     }
 }