Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- The MCP dashboard no longer pre-fills the OAuth2 token endpoint, client id and client secret. The form shows hints instead, and warns when the token endpoint is not HTTPS

### Added

- `ParameterCustomizer` now receive the type-use annotation data declared on a value wrapped in an `Optional`, so annotations such as `Optional<@MyAnnotation Foo>` can be read natively by it

## [3.1.1] - 2026-09-06

### Security
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -115,7 +118,13 @@ public class DelegatingMethodParameter extends MethodParameter {
* @param field the field
* @param isNotRequired the is required
*/
DelegatingMethodParameter(MethodParameter delegate, String parameterName, Annotation[] additionalParameterAnnotations, Annotation[] methodAnnotations, boolean isParameterObject, Field field, boolean isNotRequired) {
DelegatingMethodParameter(MethodParameter delegate,
String parameterName,
Annotation[] additionalParameterAnnotations,
Annotation[] methodAnnotations,
boolean isParameterObject,
Field field,
boolean isNotRequired) {
super(delegate);
this.delegate = delegate;
this.field = field;
Expand All @@ -137,7 +146,9 @@ public class DelegatingMethodParameter extends MethodParameter {
* @return the method parameter [ ]
*/
public static MethodParameter[] customize(String[] pNames, MethodParameter[] parameters,
Optional<List<DelegatingMethodParameterCustomizer>> optionalDelegatingMethodParameterCustomizers, MethodParameterPojoExtractor methodParameterPojoExtractor, boolean defaultFlatParamObject) {
Optional<List<DelegatingMethodParameterCustomizer>> optionalDelegatingMethodParameterCustomizers,
MethodParameterPojoExtractor methodParameterPojoExtractor,
boolean defaultFlatParamObject) {
List<MethodParameter> explodedParameters = new ArrayList<>();
for (int i = 0; i < parameters.length; ++i) {
MethodParameter p = parameters[i];
Expand All @@ -155,7 +166,7 @@ public static MethodParameter[] customize(String[] pNames, MethodParameter[] par
}
else {
String name = pNames != null ? pNames[i] : p.getParameterName();
explodedParameters.add(new DelegatingMethodParameter(p, name, null, null, false, null, false));
explodedParameters.add(new DelegatingMethodParameter(p, name, getTypeUseAnnotations(p), null, false, null, false));
}
}
return explodedParameters.toArray(new MethodParameter[0]);
Expand All @@ -166,7 +177,8 @@ public static MethodParameter[] customize(String[] pNames, MethodParameter[] par
* given containing class.
*
* @param methodParameter the method parameter
* @param containingClass a specific containing class (potentially a subclass of the declaring class, e.g. substituting a type variable) A copy of spring withContainingClass, to keep compatibility with older spring versions
* @param containingClass a specific containing class (potentially a subclass of the declaring class, e.g. substituting a type variable).
* A copy of spring withContainingClass, to keep compatibility with older spring versions
* @return the method parameter
* @see #getParameterType() #getParameterType()#getParameterType()#getParameterType()#getParameterType()
*/
Expand Down Expand Up @@ -318,4 +330,34 @@ public boolean isParameterObject() {
public Field getField() {
return field;
}

/**
* Collects the type-use annotations declared on a parameter's type so that customizers can natively
* read annotation data placed on the type. Annotations declared directly on the parameter type
* (for example {@code @MyAnnotation Foo}) are always collected. Annotations declared on the wrapped
* type of an {@link Optional} (for example {@code Optional<@MyAnnotation Foo>}) are also collected,
* since an {@code Optional} is unwrapped to its element type. Annotations on the element type of other
* container types (such as {@link java.util.List}) are intentionally not collected, as they apply to
* the items' schema rather than to the parameter itself.
*
* @param methodParameter the method parameter
* @return the type-use annotations, or {@code null} if none were found
*/
@Nullable
private static Annotation[] getTypeUseAnnotations(MethodParameter methodParameter) {
int index = methodParameter.getParameterIndex();
if (index < 0)
return null;
Parameter[] parameters = methodParameter.getExecutable().getParameters();
if (index >= parameters.length)
return null;
AnnotatedType annotatedType = parameters[index].getAnnotatedType();
List<Annotation> annotations = new ArrayList<>(Arrays.asList(annotatedType.getAnnotations()));
if (Optional.class.isAssignableFrom(methodParameter.getParameterType())
&& annotatedType instanceof AnnotatedParameterizedType annotatedParameterizedType) {
for (AnnotatedType typeArgument : annotatedParameterizedType.getAnnotatedActualTypeArguments())
annotations.addAll(Arrays.asList(typeArgument.getAnnotations()));
}
return annotations.isEmpty() ? null : annotations.toArray(new Annotation[0]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,19 @@

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
public class HelloController {

@CustomizedOperation
@Operation(description = "Some operation")
@GetMapping("/example/{test}")
public ApiType test(@PathVariable @CustomizedParameter @Parameter(description = "Parameter description") String test) {
public ApiType test(@PathVariable @CustomizedParameter @Parameter(description = "Parameter description") String test,
@RequestParam @Parameter(description = "Parameter description") Optional<@CustomizedParameter String> optionalTest) {
return new ApiType();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@

package test.org.springdoc.api.v30.app70.customizer;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomizedParameter {
String addition() default "customized parameter!";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@

package test.org.springdoc.api.v30.app70.customizer;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomizedProperty {
String addition() default "customized property!";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
package test.org.springdoc.api.v30.app70.model;

import java.time.Duration;
import java.util.Optional;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -36,4 +37,8 @@ public class ApiType {
@JsonProperty("someProperty")
private Duration someProperty;

@Schema(description = "Test description")
@JsonProperty("someOptionalProperty")
private Optional<@CustomizedProperty Duration> someOptionalProperty;

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,19 @@

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
public class HelloController {

@CustomizedOperation
@Operation(description = "Some operation")
@GetMapping("/example/{test}")
public ApiType test(@PathVariable @CustomizedParameter @Parameter(description = "Parameter description") String test) {
public ApiType test(@PathVariable @CustomizedParameter @Parameter(description = "Parameter description") String test,
@RequestParam @Parameter(description = "Parameter description") Optional<@CustomizedParameter String> optionalTest) {
return new ApiType();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@

package test.org.springdoc.api.v31.app70.customizer;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomizedParameter {
String addition() default "customized parameter!";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@

package test.org.springdoc.api.v31.app70.customizer;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomizedProperty {
String addition() default "customized property!";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
package test.org.springdoc.api.v31.app70.model;

import java.time.Duration;
import java.util.Optional;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -36,4 +37,8 @@ public class ApiType {
@JsonProperty("someProperty")
private Duration someProperty;

@Schema(description = "Test description")
@JsonProperty("someOptionalProperty")
private Optional<@CustomizedProperty Duration> someOptionalProperty;

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
"schema": {
"type": "string"
}
},
{
"name": "optionalTest",
"in": "query",
"description": "Parameter description, customized parameter!",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
Expand All @@ -53,6 +62,11 @@
"type": "string",
"properties": {},
"format": "duration"
},
"someOptionalProperty": {
"type": "string",
"properties": {},
"format": "duration"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
"schema": {
"type": "string"
}
},
{
"name": "optionalTest",
"in": "query",
"description": "Parameter description, customized parameter!",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
Expand Down Expand Up @@ -54,6 +63,12 @@
"format": "duration",
"description": "Test description",
"properties": {}
},
"someOptionalProperty": {
"type": "string",
"format": "duration",
"description": "Test description",
"properties": {}
}
}
}
Expand Down
Loading