Repository: springdoc/springdoc-openapi (java)

Issue:
Automatically add required if a field is @notNull or @NotBlank
I have a project with Swagger 3, I noticed a different behavior compared to Swagger 2 and I think it's a bug because with a structure of a certain type, I can make the call to the API, but Swagger doesn't allow me to do it.

I have this API:
```
    @PostMapping(path = "/testOne", consumes = MediaType.APPLICATION_JSON_VALUE)
    ResponseEntity<String> testOne(@ParameterObject @Valid ClassOne test) {
        return ResponseEntity.ok("ok");
    }
```

```
@Data
public class ClassOne {

    @Valid
    private ClassTwo description;

}
```


```
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ClassTwo {

    @NotNull
    private String name;
}
```

As you can see, `ClassOne` has an object with the `@Valid` annotation and I am not obliged to pass it (because maybe I want to pass other attributes that I have not added in this example). However, the `@Valid` annotation ensures that if I pass any attribute that is inside that class, then it must respect the rules that I indicate inside (@NotNull etc.). The problem is that on the swagger it says required, even if I do not indicate required and therefore I cannot make the call to the API. This is therefore a problem as I said before, because I may want to send other attributes of `ClassOne`.
This problem occurs only in the case of `@PraameterObject`, while if I use `@RequestBody`, it is not mandatory.

In my opinion swagger should not automatically add requird if an attribute has the annotation `@NotNull` or `@NotBlank`, it should refer to what the user indicates with the annotation @Schema

attached is a zip with an example.
[demo.zip](https://github.com/user-attachments/files/18166665/demo.zip)


Pull request:
Obey annotations when flattening ParameterObject fields. Fixes #2817
When creating the flattened parameter definitions for an item annotated with @ParameterObject, the @Schema and @Property annotations on any fields prior to the final child-node of each branch of the parameter tree are not taken into consideration. This results in the field name in the code being used even where annotations may override that, prevents the fields being hidden where one of the parent fields is marked as hidden but the child field isn't hidden, and marks a field as mandatory even where the field would only be mandatory if the parent object had been declared through a sibling of the target field being set. To overcome this, whilst the flattened parameter map is being built, each field is now being inspected for a @Parameter annotation or - where the @Parameter annotation isn't found - a @Schema annotation. Where a custom name is included in either of those annotations then the overridden field name is used in the flattened definition. If either annotation declares the field as hidden then the field and all child fields are removed from the resulting definition, and a field is no longer set as mandatory unless the parent field was also declared as mandatory, or resolved as non-null and was set in an automatic state, or the child field is specifically set as required in the Schema or Parameter annotation.

Patch:
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/DelegatingMethodParameter.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/DelegatingMethodParameter.java
index bfd4b031b..94b744912 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/DelegatingMethodParameter.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/DelegatingMethodParameter.java
@@ -93,6 +93,11 @@ public class DelegatingMethodParameter extends MethodParameter {
 	 */
 	private final Annotation[] methodAnnotations;
 
+	/**
+	 * The annotations to mask from the list of annotations on this method parameter.
+	 */
+	private final Annotation[] maskedAnnotations;
+
 	/**
 	 * The Is not required.
 	 */
@@ -105,17 +110,19 @@ public class DelegatingMethodParameter extends MethodParameter {
 	 * @param parameterName                  the parameter name
 	 * @param additionalParameterAnnotations the additional parameter annotations
 	 * @param methodAnnotations              the method annotations
+	 * @param maskedAnnotations              any annotations that should not be included in the final list of annotations
 	 * @param isParameterObject              the is parameter object
 	 * @param isNotRequired                  the is required
 	 */
-	DelegatingMethodParameter(MethodParameter delegate, String parameterName, Annotation[] additionalParameterAnnotations, Annotation[] methodAnnotations, boolean isParameterObject, boolean isNotRequired) {
+	DelegatingMethodParameter(MethodParameter delegate, String parameterName, Annotation[] additionalParameterAnnotations, Annotation[] methodAnnotations, Annotation[] maskedAnnotations, boolean isParameterObject, boolean isNotRequired) {
 		super(delegate);
 		this.delegate = delegate;
 		this.additionalParameterAnnotations = additionalParameterAnnotations;
 		this.parameterName = parameterName;
 		this.isParameterObject = isParameterObject;
 		this.isNotRequired = isNotRequired;
-		this.methodAnnotations =methodAnnotations;
+		this.methodAnnotations = methodAnnotations;
+		this.maskedAnnotations = maskedAnnotations;
 	}
 
 	/**
@@ -146,7 +153,7 @@ public class DelegatingMethodParameter extends MethodParameter {
 			}
 			else {
 				String name = pNames != null ? pNames[i] : p.getParameterName();
-				explodedParameters.add(new DelegatingMethodParameter(p, name, null, null, false, false));
+				explodedParameters.add(new DelegatingMethodParameter(p, name, null, null, null, false, false));
 			}
 		}
 		return explodedParameters.toArray(new MethodParameter[0]);
@@ -179,7 +186,15 @@ public class DelegatingMethodParameter extends MethodParameter {
 	@NonNull
 	public Annotation[] getParameterAnnotations() {
 		Annotation[] methodAnnotations = ArrayUtils.addAll(delegate.getParameterAnnotations(), this.methodAnnotations);
-		return ArrayUtils.addAll(methodAnnotations, additionalParameterAnnotations);
+		methodAnnotations = ArrayUtils.addAll(methodAnnotations, additionalParameterAnnotations);
+		if (maskedAnnotations == null) {
+			return methodAnnotations;
+		} else {
+			List<Annotation> maskedAnnotationList = List.of(maskedAnnotations);
+			return Arrays.stream(methodAnnotations)
+					.filter(annotation -> !maskedAnnotationList.contains(annotation))
+					.toArray(Annotation[]::new);
+		}
 	}
 
 	@Override
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/MethodParameterPojoExtractor.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/MethodParameterPojoExtractor.java
index 3b7033e43..04123a705 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/MethodParameterPojoExtractor.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/MethodParameterPojoExtractor.java
@@ -50,10 +50,13 @@ import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import io.swagger.v3.core.util.PrimitiveType;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springdoc.core.service.AbstractRequestService;
 
 import org.springframework.core.GenericTypeResolver;
 import org.springframework.core.MethodParameter;
@@ -63,7 +66,7 @@ import static org.springdoc.core.utils.Constants.DOT;
 /**
  * The type Method parameter pojo extractor.
  *
- * @author bnasslahsen
+ * @author bnasslahsen, michael.clarke
  */
 public class MethodParameterPojoExtractor {
 
@@ -113,7 +116,7 @@ public class MethodParameterPojoExtractor {
 	 * @return the stream
 	 */
 	static Stream<MethodParameter> extractFrom(Class<?> clazz) {
-		return extractFrom(clazz, "");
+		return extractFrom(clazz, "", true);
 	}
 
 	/**
@@ -121,12 +124,13 @@ public class MethodParameterPojoExtractor {
 	 *
 	 * @param clazz           the clazz
 	 * @param fieldNamePrefix the field name prefix
+	 * @param parentRequired  whether the field that hold the class currently being inspected was required or optional
 	 * @return the stream
 	 */
-	private static Stream<MethodParameter> extractFrom(Class<?> clazz, String fieldNamePrefix) {
+	private static Stream<MethodParameter> extractFrom(Class<?> clazz, String fieldNamePrefix, boolean parentRequired) {
 		return allFieldsOf(clazz).stream()
 				.filter(field -> !field.getType().equals(clazz))
-				.flatMap(f -> fromGetterOfField(clazz, f, fieldNamePrefix))
+				.flatMap(f -> fromGetterOfField(clazz, f, fieldNamePrefix, parentRequired))
 				.filter(Objects::nonNull);
 	}
 
@@ -136,20 +140,95 @@ public class MethodParameterPojoExtractor {
 	 * @param paramClass      the param class
 	 * @param field           the field
 	 * @param fieldNamePrefix the field name prefix
+	 * @param parentRequired  whether the field that holds the class currently being examined was required or optional
 	 * @return the stream
 	 */
-	private static Stream<MethodParameter> fromGetterOfField(Class<?> paramClass, Field field, String fieldNamePrefix) {
+	private static Stream<MethodParameter> fromGetterOfField(Class<?> paramClass, Field field, String fieldNamePrefix, boolean parentRequired) {
 		Class<?> type = extractType(paramClass, field);
 
 		if (Objects.isNull(type))
 			return Stream.empty();
 
 		if (isSimpleType(type))
-			return fromSimpleClass(paramClass, field, fieldNamePrefix);
+			return fromSimpleClass(paramClass, field, fieldNamePrefix, parentRequired);
 		else {
-			String prefix = fieldNamePrefix + field.getName() + DOT;
-			return extractFrom(type, prefix);
+			Parameter parameter = field.getAnnotation(Parameter.class);
+			Schema schema = field.getAnnotation(Schema.class);
+			boolean visible = resolveVisible(parameter, schema);
+			if (!visible) {
+				return Stream.empty();
+			}
+			String prefix = fieldNamePrefix + resolveName(parameter, schema).orElse(field.getName()) + DOT;
+			boolean notNullAnnotationsPresent = AbstractRequestService.hasNotNullAnnotation(Arrays.stream(field.getDeclaredAnnotations())
+					.map(Annotation::annotationType)
+					.map(Class::getSimpleName)
+					.collect(Collectors.toSet()));
+			return extractFrom(type, prefix, parentRequired && resolveRequired(schema, parameter, !notNullAnnotationsPresent));
+		}
+	}
+
+	private static Optional<String> resolveName(Parameter parameter, Schema schema) {
+		if (parameter != null) {
+			return resolveNameFromParameter(parameter);
+		}
+		if (schema != null) {
+			return resolveNameFromSchema(schema);
+		}
+		return Optional.empty();
+	}
+
+	private static Optional<String> resolveNameFromParameter(Parameter parameter) {
+		if (parameter.name().isEmpty()) {
+			return Optional.empty();
+		}
+		return Optional.of(parameter.name());
+	}
+
+	private static Optional<String> resolveNameFromSchema(Schema schema) {
+		if (schema.name().isEmpty()) {
+			return Optional.empty();
+		}
+		return Optional.of(schema.name());
+	}
+
+	private static boolean resolveVisible(Parameter parameter, Schema schema) {
+		if (parameter != null) {
+			return !parameter.hidden();
+		}
+		if (schema != null) {
+			return !schema.hidden();
 		}
+		return true;
+	}
+
+	private static boolean resolveRequired(Schema schema, Parameter parameter, boolean nullable) {
+		if (parameter != null) {
+			return resolveRequiredFromParameter(parameter, nullable);
+		}
+		if (schema != null) {
+			return resolveRequiredFromSchema(schema, nullable);
+		}
+		return !nullable;
+	}
+
+	private static boolean resolveRequiredFromParameter(Parameter parameter, boolean nullable) {
+		if (parameter.required()) {
+			return true;
+		}
+		return !nullable;
+	}
+
+	private static boolean resolveRequiredFromSchema(Schema schema, boolean nullable) {
+		if (schema.required()) {
+			return true;
+		}
+		else if (schema.requiredMode() == Schema.RequiredMode.REQUIRED) {
+			return true;
+		}
+		else if (schema.requiredMode() == Schema.RequiredMode.NOT_REQUIRED) {
+			return false;
+		}
+		return !nullable;
 	}
 
 	/**
@@ -181,18 +260,32 @@ public class MethodParameterPojoExtractor {
 	 * @param fieldNamePrefix the field name prefix
 	 * @return the stream
 	 */
-	private static Stream<MethodParameter> fromSimpleClass(Class<?> paramClass, Field field, String fieldNamePrefix) {
+	private static Stream<MethodParameter> fromSimpleClass(Class<?> paramClass, Field field, String fieldNamePrefix, boolean isParentRequired) {
 		Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
 		try {
 			Parameter parameter = field.getAnnotation(Parameter.class);
-			boolean isNotRequired = parameter == null || !parameter.required();
+			Schema schema = field.getAnnotation(Schema.class);
+			boolean visible = resolveVisible(parameter, schema);
+			if (!visible) {
+				return Stream.empty();
+			}
+
+			boolean forcedRequired = resolveRequired(schema, parameter, true);
+
+			boolean isNotRequired = !((isParentRequired || forcedRequired) && resolveRequired(schema, parameter, !AbstractRequestService.hasNotNullAnnotation(Arrays.stream(fieldAnnotations)
+					.map(Annotation::annotationType)
+					.map(Class::getSimpleName)
+					.collect(Collectors.toSet()))));
+			Annotation[] notNullFieldAnnotations = Arrays.stream(fieldAnnotations)
+					.filter(annotation -> AbstractRequestService.hasNotNullAnnotation(List.of(annotation.annotationType().getSimpleName())))
+					.toArray(Annotation[]::new);
 			if (paramClass.getSuperclass() != null && paramClass.isRecord()) {
 				return Stream.of(paramClass.getRecordComponents())
 						.filter(d -> d.getName().equals(field.getName()))
 						.map(RecordComponent::getAccessor)
 						.map(method -> new MethodParameter(method, -1))
 						.map(methodParameter -> DelegatingMethodParameter.changeContainingClass(methodParameter, paramClass))
-						.map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), fieldAnnotations, param.getMethodAnnotations(), true, isNotRequired));
+						.map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), fieldAnnotations, param.getMethodAnnotations(), notNullFieldAnnotations, true, isNotRequired));
 
 			}
 			else
@@ -202,7 +295,7 @@ public class MethodParameterPojoExtractor {
 						.filter(Objects::nonNull)
 						.map(method -> new MethodParameter(method, -1))
 						.map(methodParameter -> DelegatingMethodParameter.changeContainingClass(methodParameter, paramClass))
-						.map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), fieldAnnotations, param.getMethodAnnotations(), true, isNotRequired));
+						.map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), fieldAnnotations, param.getMethodAnnotations(), notNullFieldAnnotations, true, isNotRequired));
 		}
 		catch (IntrospectionException e) {
 			return Stream.of();
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
index 0ac771b04..60ec6963f 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
@@ -584,6 +584,9 @@ public abstract class AbstractRequestService {
 		if (parameter.getRequired() == null)
 			parameter.setRequired(parameterInfo.isRequired());
 
+		if (Boolean.TRUE.equals(parameter.getRequired()) && parameterInfo.getMethodParameter() instanceof DelegatingMethodParameter delegatingMethodParameter && delegatingMethodParameter.isNotRequired())
+			parameter.setRequired(false);
+
 		if (containsDeprecatedAnnotation(parameterInfo.getMethodParameter().getParameterAnnotations()))
 			parameter.setDeprecated(true);
 
@@ -616,7 +619,7 @@ public abstract class AbstractRequestService {
 		Map<String, Annotation> annos = new HashMap<>();
 		if (annotations != null)
 			annotations.forEach(annotation -> annos.put(annotation.annotationType().getSimpleName(), annotation));
-		boolean annotationExists = Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annos::containsKey);
+		boolean annotationExists = hasNotNullAnnotation(annos.keySet());
 		if (annotationExists)
 			parameter.setRequired(true);
 		Schema<?> schema = parameter.getSchema();
@@ -643,7 +646,7 @@ public abstract class AbstractRequestService {
 					.filter(annotation -> io.swagger.v3.oas.annotations.parameters.RequestBody.class.equals(annotation.annotationType()))
 					.anyMatch(annotation -> ((io.swagger.v3.oas.annotations.parameters.RequestBody) annotation).required());
 		}
-		boolean validationExists = Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annos::containsKey);
+		boolean validationExists = hasNotNullAnnotation(annos.keySet());
 
 		if (validationExists || (!isOptional && (springRequestBodyRequired || swaggerRequestBodyRequired)))
 			requestBody.setRequired(true);
@@ -840,5 +843,15 @@ public abstract class AbstractRequestService {
 		}
 		return false;
 	}
+
+    /**
+     * Check if the parameter has any of the annotations that make it non-optional
+     *
+     * @param annotationSimpleNames the annotation simple class named, e.g. NotNull
+     * @return whether any of the known NotNull annotations are present
+     */
+    public static boolean hasNotNullAnnotation(Collection<String> annotationSimpleNames) {
+        return Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annotationSimpleNames::contains);
+    }
 	
 }


Test patch:
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/ParameterController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/ParameterController.java
new file mode 100644
index 000000000..c891b3a90
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/ParameterController.java
@@ -0,0 +1,95 @@
+/*
+ *
+ *  *
+ *  *  *
+ *  *  *  *
+ *  *  *  *  *
+ *  *  *  *  *  * Copyright 2019-2024 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 test.org.springdoc.api.v30.app233;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import org.springdoc.core.annotations.ParameterObject;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ParameterController {
+
+    @GetMapping("/hidden-parent")
+    public void nestedParameterObjectWithHiddenParentField(@ParameterObject ParameterObjectWithHiddenField parameters) {
+
+    }
+
+    public record ParameterObjectWithHiddenField(
+            @Schema(hidden = true) NestedParameterObject schemaHiddenNestedParameterObject,
+            @Parameter(hidden = true) NestedParameterObject parameterHiddenNestedParameterObject,
+            NestedParameterObject visibleNestedParameterObject
+    ) {
+
+    }
+
+    public record NestedParameterObject(
+            String parameterField) {
+    }
+
+    @GetMapping("/renamed-parent")
+    public void nestedParameterObjectWithRenamedParentField(@ParameterObject ParameterObjectWithRenamedField parameters) {
+
+    }
+
+    public record ParameterObjectWithRenamedField(
+            @Schema(name = "schemaRenamed") NestedParameterObject schemaRenamedNestedParameterObject,
+            @Parameter(name = "parameterRenamed") NestedParameterObject parameterRenamedNestedParameterObject,
+            NestedParameterObject originalNameNestedParameterObject
+    ) {
+
+    }
+
+    @GetMapping("/optional-parent")
+    public void nestedParameterObjectWithOptionalParentField(@Valid @ParameterObject MultiFieldParameterObject parameters) {
+
+    }
+
+    public record MultiFieldParameterObject(
+            @Valid @Schema(requiredMode = RequiredMode.REQUIRED) @NotNull MultiFieldNestedParameterObject requiredNotNullParameterObject,
+            @Valid @Schema(requiredMode = RequiredMode.REQUIRED) MultiFieldNestedParameterObject requiredNoValidationParameterObject,
+            @Valid @Schema(requiredMode = RequiredMode.NOT_REQUIRED) @NotNull MultiFieldNestedParameterObject notRequiredNotNullParameterObject,
+            @Valid @Schema(requiredMode = RequiredMode.NOT_REQUIRED) MultiFieldNestedParameterObject notRequiredNoValidationParameterObject,
+            @Valid @NotNull MultiFieldNestedParameterObject noSchemaNotNullParameterObject,
+            @Valid MultiFieldNestedParameterObject noSchemaNoValidationParameterObject) {
+
+    }
+
+    public record MultiFieldNestedParameterObject (
+            @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotNull String requiredNotNullField,
+            @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String requiredNoValidationField,
+            @Schema(requiredMode = RequiredMode.NOT_REQUIRED) @NotNull String notRequiredNotNullField,
+            @Schema(requiredMode = RequiredMode.NOT_REQUIRED) String notRequiredNoValidationField,
+            @NotNull String noSchemaNotNullField,
+            String noSchemaNoValidationField) {
+    }
+
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/SpringDocApp233Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/SpringDocApp233Test.java
new file mode 100644
index 000000000..4480482d1
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app233/SpringDocApp233Test.java
@@ -0,0 +1,185 @@
+/*
+ *
+ *  *
+ *  *  *
+ *  *  *  *
+ *  *  *  *  *
+ *  *  *  *  *  * Copyright 2019-2024 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 test.org.springdoc.api.v30.app233;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.jayway.jsonpath.JsonPath;
+import net.minidev.json.JSONArray;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.springdoc.core.utils.Constants;
+import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author bnasslahsen, michael.clarke
+ */
+class SpringDocApp233Test extends AbstractSpringDocV30Test {
+
+    @CsvSource({"requiredNotNullParameterObject.requiredNotNullField, true",
+            "requiredNotNullParameterObject.requiredNoValidationField, true",
+            "requiredNotNullParameterObject.notRequiredNotNullField, false",
+            "requiredNotNullParameterObject.notRequiredNoValidationField, false",
+            "requiredNotNullParameterObject.noSchemaNotNullField, true",
+            "requiredNotNullParameterObject.noSchemaNoValidationField, false",
+            "requiredNoValidationParameterObject.requiredNotNullField, true",
+            "requiredNoValidationParameterObject.requiredNoValidationField, true",
+            "requiredNoValidationParameterObject.notRequiredNotNullField, false",
+            "requiredNoValidationParameterObject.notRequiredNoValidationField, false",
+            "requiredNoValidationParameterObject.noSchemaNotNullField, true",
+            "requiredNoValidationParameterObject.noSchemaNoValidationField, false",
+            "notRequiredNotNullParameterObject.requiredNotNullField, true",
+            "notRequiredNotNullParameterObject.requiredNoValidationField, true",
+            "notRequiredNotNullParameterObject.notRequiredNotNullField, false",
+            "notRequiredNotNullParameterObject.notRequiredNoValidationField, false",
+            "notRequiredNotNullParameterObject.noSchemaNotNullField, false",
+            "notRequiredNotNullParameterObject.noSchemaNoValidationField, false",
+            "notRequiredNoValidationParameterObject.requiredNotNullField, true",
+            "notRequiredNoValidationParameterObject.requiredNoValidationField, true",
+            "notRequiredNoValidationParameterObject.notRequiredNotNullField, false",
+            "notRequiredNoValidationParameterObject.notRequiredNoValidationField, false",
+            "notRequiredNoValidationParameterObject.noSchemaNotNullField, false",
+            "notRequiredNoValidationParameterObject.noSchemaNoValidationField, false",
+            "noSchemaNotNullParameterObject.requiredNotNullField, true",
+            "noSchemaNotNullParameterObject.requiredNoValidationField, true",
+            "noSchemaNotNullParameterObject.notRequiredNotNullField, false",
+            "noSchemaNotNullParameterObject.notRequiredNoValidationField, false",
+            "noSchemaNotNullParameterObject.noSchemaNotNullField, true",
+            "noSchemaNotNullParameterObject.noSchemaNoValidationField, false",
+            "noSchemaNoValidationParameterObject.requiredNotNullField, true",
+            "noSchemaNoValidationParameterObject.requiredNoValidationField, true",
+            "noSchemaNoValidationParameterObject.notRequiredNotNullField, false",
+            "noSchemaNoValidationParameterObject.notRequiredNoValidationField, false",
+            "noSchemaNoValidationParameterObject.noSchemaNotNullField, false",
+            "noSchemaNoValidationParameterObject.noSchemaNoValidationField, false"})
+    @ParameterizedTest
+    void shouldHaveCorrectRequireStatus(String field, String required) throws Exception {
+        MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)).andExpect(status().isOk()).andReturn();
+        String result = mockMvcResult.getResponse().getContentAsString();
+
+        String requiredMode = ((JSONArray) JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[?(@.name == '" + field + "')].required")).get(0).toString();
+        assertThat(requiredMode).isEqualTo(required);
+    }
+
+    @Test
+    void shouldMatchSwaggerFieldRequirementsMatchJavaValidation() throws Exception {
+        MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)).andExpect(status().isOk()).andReturn();
+        String result = mockMvcResult.getResponse().getContentAsString();
+
+        JSONArray allFieldsJsonArray = JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[*].name");
+        List<String> allFields = allFieldsJsonArray.stream().map(Object::toString).toList();
+
+        // check we get no validation failures when all mandatory fields are present
+        verifySwaggerFieldRequirementsMatchJavaValidation(allFields, List.of());
+
+        JSONArray mandatoryFieldsJsonArray = JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[?(@.required == true)].name");
+        List<String> mandatoryFields = mandatoryFieldsJsonArray.stream().map(Object::toString).toList();
+
+        // check validation failures when each individual mandatory field is missing
+        for (String mandatoryField : mandatoryFields) {
+            List<String> filteredFields = allFields.stream()
+                    .filter(field -> !field.equals(mandatoryField))
+                    .toList();
+
+            List<String> expectedErrors = Stream.of(mandatoryField)
+                    // Fields using Swagger annotations to drive required status but not using Java validation to enforce it so don't cause validation errors
+                    .filter(field -> !field.endsWith("requiredNullableField"))
+                    .filter(field -> !field.endsWith("requiredNoValidationField"))
+                    // the error is returned prefixed with the query parameter name, so add it to the expected error message
+                    .map(field -> "multiFieldParameterObject." + field)
+                    .toList();
+            verifySwaggerFieldRequirementsMatchJavaValidation(filteredFields, expectedErrors);
+        }
+
+
+        JSONArray nonMandatoryFieldsJsonArray = JsonPath.parse(result).read("$.paths.['/optional-parent'].get.parameters[?(@.required == false)].name");
+        List<String> nonMandatoryFields = nonMandatoryFieldsJsonArray.stream().map(Object::toString).toList();
+
+        // check validation failures for any individual non-mandatory fields being missed
+        for (String nonMandatoryField : nonMandatoryFields) {
+            List<String> filteredFields = allFields.stream()
+                    .filter(field -> !field.equals(nonMandatoryField))
+                    .toList();
+
+            List<String> expectedErrors = Stream.of(nonMandatoryField)
+                    // Fields that are mandatory but either have nullable parent fields so are excluded in swagger or are marked as not required so do cause validation errors
+                    .filter(field -> field.endsWith("NotNullField"))
+                    // the error is returned prefixed with the query parameter name, so add it to the expected error message
+                    .map(field -> "multiFieldParameterObject." + field)
+                    .toList();
+            verifySwaggerFieldRequirementsMatchJavaValidation(filteredFields, expectedErrors);
+        }
+    }
+
+    private void verifySwaggerFieldRequirementsMatchJavaValidation(Collection<String> requestFields, Collection<String> expectedErrorFields) throws Exception {
+        MockHttpServletRequestBuilder request = get("/optional-parent");
+        for (String mandatoryField : requestFields) {
+            request.queryParam(mandatoryField, mandatoryField + ".value");
+        }
+
+        mockMvc.perform(request)
+                .andExpect(result -> {
+
+                    Set<String> errorFields = Optional.ofNullable(result.getResolvedException())
+                            .map(MethodArgumentNotValidException.class::cast)
+                            .map(MethodArgumentNotValidException::getBindingResult)
+                            .map(BindingResult::getFieldErrors)
+                            .stream()
+                            .flatMap(Collection::stream)
+                            .map(field -> field.getObjectName() + "." + field.getField())
+                            .collect(Collectors.toSet());
+
+
+                    assertThat(errorFields).containsExactlyElementsOf(expectedErrorFields);
+
+                    assertThat(result.getResponse().getStatus()).isEqualTo(expectedErrorFields.isEmpty() ? 200 : 400);
+                });
+    }
+
+
+    @SpringBootApplication
+    static class SpringDocTestApp {
+
+    }
+
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app233.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app233.json
new file mode 100644
index 000000000..d0cb5bfd5
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app233.json
@@ -0,0 +1,381 @@
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "OpenAPI definition",
+    "version": "v0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost",
+      "description": "Generated server url"
+    }
+  ],
+  "paths": {
+    "/renamed-parent": {
+      "get": {
+        "tags": [
+          "parameter-controller"
+        ],
+        "operationId": "nestedParameterObjectWithRenamedParentField",
+        "parameters": [
+          {
+            "name": "schemaRenamed.parameterField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "parameterRenamed.parameterField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "originalNameNestedParameterObject.parameterField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/optional-parent": {
+      "get": {
+        "tags": [
+          "parameter-controller"
+        ],
+        "operationId": "nestedParameterObjectWithOptionalParentField",
+        "parameters": [
+          {
+            "name": "requiredNotNullParameterObject.requiredNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNotNullParameterObject.requiredNoValidationField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNotNullParameterObject.notRequiredNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNotNullParameterObject.notRequiredNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNotNullParameterObject.noSchemaNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNotNullParameterObject.noSchemaNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNoValidationParameterObject.requiredNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNoValidationParameterObject.requiredNoValidationField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNoValidationParameterObject.notRequiredNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNoValidationParameterObject.notRequiredNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNoValidationParameterObject.noSchemaNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "requiredNoValidationParameterObject.noSchemaNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNotNullParameterObject.requiredNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNotNullParameterObject.requiredNoValidationField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNotNullParameterObject.notRequiredNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNotNullParameterObject.notRequiredNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNotNullParameterObject.noSchemaNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNotNullParameterObject.noSchemaNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNoValidationParameterObject.requiredNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNoValidationParameterObject.requiredNoValidationField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNoValidationParameterObject.notRequiredNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNoValidationParameterObject.notRequiredNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNoValidationParameterObject.noSchemaNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "notRequiredNoValidationParameterObject.noSchemaNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNotNullParameterObject.requiredNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNotNullParameterObject.requiredNoValidationField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNotNullParameterObject.notRequiredNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNotNullParameterObject.notRequiredNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNotNullParameterObject.noSchemaNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNotNullParameterObject.noSchemaNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNoValidationParameterObject.requiredNotNullField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNoValidationParameterObject.requiredNoValidationField",
+            "in": "query",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNoValidationParameterObject.notRequiredNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNoValidationParameterObject.notRequiredNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNoValidationParameterObject.noSchemaNotNullField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "noSchemaNoValidationParameterObject.noSchemaNoValidationField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/hidden-parent": {
+      "get": {
+        "tags": [
+          "parameter-controller"
+        ],
+        "operationId": "nestedParameterObjectWithHiddenParentField",
+        "parameters": [
+          {
+            "name": "visibleNestedParameterObject.parameterField",
+            "in": "query",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    }
+  },
+  "components": {}
+}
\ No newline at end of file