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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

public class SpecFilter {

private OpenAPI _ctxOpenAPI;
public OpenAPI filter(OpenAPI openAPI, OpenAPISpecFilter filter, Map<String, List<String>> params, Map<String, String> cookies, Map<String, List<String>> headers) {
OpenAPI filteredOpenAPI = filterOpenAPI(filter, openAPI, params, cookies, headers);
if (filteredOpenAPI == null) {
Expand Down Expand Up @@ -399,6 +400,26 @@ private void addPathItemSchemaRef(PathItem pathItem, Set<String> referencedDefin
}

private void addApiResponseSchemaRef(ApiResponse response, Set<String> referencedDefinitions) {
if (response == null) return;
String respRef = response.get$ref();
if (respRef != null && !respRef.isEmpty() && _ctxOpenAPI != null) {
String name = (String) RefUtils.extractSimpleName(respRef).getLeft();
referencedDefinitions.add(name);
Components comps = _ctxOpenAPI.getComponents();
ApiResponse resolved = (comps != null && comps.getResponses() != null)
? comps.getResponses().get(name)
: null;
if (resolved != null) {
if (resolved.getHeaders() != null) {
for (Header h : resolved.getHeaders().values()) {
addHeaderSchemaRef(h, referencedDefinitions);
}
}
addContentSchemaRef(resolved.getContent(), referencedDefinitions);
}
return;
}

if (response.getHeaders() != null) {
for (String keyHeaders : response.getHeaders().keySet()) {
Header header = response.getHeaders().get(keyHeaders);
Expand Down Expand Up @@ -469,7 +490,7 @@ private void addComponentsSchemaRef(Components components, Set<String> reference
}

protected OpenAPI removeBrokenReferenceDefinitions(OpenAPI openApi) {

this._ctxOpenAPI = openApi;
if (openApi == null || openApi.getComponents() == null || openApi.getComponents().getSchemas() == null) {
return openApi;
}
Expand Down Expand Up @@ -499,6 +520,7 @@ protected OpenAPI removeBrokenReferenceDefinitions(OpenAPI openApi) {
.retainAll(referencedDefinitions.stream()
.map(s -> (String) RefUtils.extractSimpleName(s).getLeft())
.collect(Collectors.toSet()));
this._ctxOpenAPI = null;
return openApi;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1787,6 +1787,8 @@ protected boolean applyBeanValidatorAnnotations(Schema property, Annotation[] an
return modified;
}
}
// expand composed constraint meta-annotations (e.g., @Min/@Max on custom annotations)
annotations = expandValidationMetaAnnotations(annotations);
Map<String, Annotation> annos = new HashMap<>();
if (annotations != null) {
for (Annotation anno : annotations) {
Expand Down Expand Up @@ -1979,6 +1981,8 @@ protected boolean checkGroupValidation(Class[] groups, Set<Class> invocationGrou
}

protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
// expand composed constraint meta-annotations (e.g., @Min/@Max on custom annotations)
annotations = expandValidationMetaAnnotations(annotations);
Map<String, Annotation> annos = new HashMap<>();
boolean modified = false;
if (annotations != null) {
Expand Down Expand Up @@ -2082,6 +2086,42 @@ protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotat
return modified;
}

/**
* Expands provided annotations to include bean-validation constraint annotations present as meta-annotations
* on custom annotations (i.e., composed constraints like a custom @ValidStoreId annotated with @Min/@Max).
* Only javax.validation.constraints annotations are added to avoid unrelated meta-annotations.
*/
private Annotation[] expandValidationMetaAnnotations(Annotation[] annotations) {
if (annotations == null || annotations.length == 0) {
return annotations;
}
Map<String, Annotation> merged = new LinkedHashMap<>();
for (Annotation a : annotations) {
if (a != null) {
merged.put(a.annotationType().getName(), a);
}
}
try {
for (Annotation a : annotations) {
if (a == null) continue;
Annotation[] metas = a.annotationType().getAnnotations();
if (metas == null) continue;
for (Annotation meta : metas) {
if (meta == null) continue;
String name = meta.annotationType().getName();
// include only standard bean validation constraint annotations
if (name != null && name.startsWith("javax.validation.constraints")) {
merged.putIfAbsent(name, meta);
}
}
}
} catch (Throwable t) {
// be conservative: if anything goes wrong, fall back to original annotations
return annotations;
}
return merged.values().toArray(new Annotation[0]);
}

private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConverterContext context, JsonView jsonViewAnnotation) {
final List<NamedType> types = _intr().findSubtypes(bean.getClassInfo());
if (types == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.swagger.v3.core.resolving;

import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Map;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

public class ComposedConstraintMetaAnnotationTest {

@Min(0)
@Max(999)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidStoreId {
String message() default "Invalid store ID";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

static class TestStoreDto {
@Min(0)
@Max(999)
@NotNull
private Short storeId;

@ValidStoreId
@NotNull
private Short metaStoreId;

public Short getStoreId() {
return storeId;
}

public void setStoreId(Short storeId) {
this.storeId = storeId;
}

public Short getMetaStoreId() {
return metaStoreId;
}

public void setMetaStoreId(Short metaStoreId) {
this.metaStoreId = metaStoreId;
}
}

@Test
public void readsComposedConstraintOnDtoField() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
Schema model = schemas.get("TestStoreDto");
assertNotNull(model, "Model should be resolved");
Schema meta = (Schema) model.getProperties().get("metaStoreId");
assertNotNull(meta, "metaStoreId property should exist");
// Should carry over min/max from composed constraint
assertEquals(((IntegerSchema) meta).getMinimum().intValue(), 0);
assertEquals(((IntegerSchema) meta).getMaximum().intValue(), 999);
}
}