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 @@ -148,4 +148,17 @@
* @return whether or not to ignore JsonView annotations
*/
boolean ignoreJsonView() default false;

/**
* Specifies the Bean Validation group associated with this operation.
* Validation groups (as defined by JSR-380 Bean Validation) allow a single DTO
* to be reused across multiple operations by specifying which group is active
* for a given operation (e.g., Create.class for POST, Update.class for PUT).
* When specified, this information can be used to conditionally display only
* the schema fields relevant to the active group in the generated OpenAPI documentation.
*
* @since 2.2.54
* @return the validation group for this operation
*/
Class<?> groups() default Void.class;
}
Original file line number Diff line number Diff line change
Expand Up @@ -650,4 +650,17 @@ enum SchemaResolution {
*
*/
SchemaResolution schemaResolution() default SchemaResolution.AUTO;

/**
* Provides the validation groups associated with this schema property.
* Validation groups (as defined by JSR-380 Bean Validation) allow a single DTO
* to represent different shapes depending on the operation being performed
* (e.g., Create vs Update). When specified, this information can be used to
* conditionally include or hide fields in the generated OpenAPI documentation
* based on the active validation group of the operation.
*
* @since 2.2.54
* @return the validation groups for this schema property
*/
Class<?>[] groups() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3196,6 +3196,13 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann
if (StringUtils.isNotBlank(description)) {
schema.description(description);
}
if (schemaAnnotation != null && schemaAnnotation.groups() != null && schemaAnnotation.groups().length > 0) {
java.util.List<String> groupNames = new java.util.ArrayList<>();
for (Class<?> group : schemaAnnotation.groups()) {
groupNames.add(group.getSimpleName());
}
schema.groups(groupNames);
}
String title = resolveTitle(a, annotations, schemaAnnotation);
if (StringUtils.isNotBlank(title)) {
schema.title(title);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public static boolean hasSchemaAnnotation(io.swagger.v3.oas.annotations.media.Sc
&& schema.additionalProperties().equals(io.swagger.v3.oas.annotations.media.Schema.AdditionalPropertiesValue.USE_ADDITIONAL_PROPERTIES_ANNOTATION)
&& schema.additionalPropertiesSchema().equals(Void.class)
&& schema.examples().length == 0
&& schema.groups().length == 0
) {
return false;
}
Expand Down Expand Up @@ -661,6 +662,14 @@ public static Optional<Schema> getSchemaFromAnnotation(
if (!Schema.SchemaResolution.DEFAULT.equals(schemaResolution)) {
schemaObject = existingSchema;
} else {
// apply groups before early return so the property schema carries group info
if (schema != null && schema.groups().length > 0) {
List<String> groupNames = new java.util.ArrayList<>();
for (Class<?> group : schema.groups()) {
groupNames.add(group.getSimpleName());
}
existingSchema.setGroups(groupNames);
}
return Optional.of(existingSchema);
}
}
Expand Down Expand Up @@ -923,6 +932,14 @@ public static Optional<Schema> getSchemaFromAnnotation(
}
}

if (schema.groups().length > 0) {
List<String> groupNames = new java.util.ArrayList<>();
for (Class<?> group : schema.groups()) {
groupNames.add(group.getSimpleName());
}
schemaObject.setGroups(groupNames);
}

return Optional.of(schemaObject);
}

Expand Down Expand Up @@ -2885,6 +2902,14 @@ public SchemaResolution schemaResolution() {
return master.schemaResolution();
}

@Override
public Class<?>[] groups() {
if (master.groups().length > 0 || patch.groups().length == 0) {
return master.groups();
}
return patch.groups();
}

};

return (io.swagger.v3.oas.annotations.media.Schema) schema;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.swagger.v3.core.resolving;

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

import java.util.List;
import java.util.Map;

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

/**
* Tests for issue #4928 - Validation Grouping support in @Schema.
* Verifies that groups() in @Schema annotation are correctly resolved
* into the Schema model object's groups field.
*/
public class ValidationGroupTest {

// --- Marker interfaces representing validation groups ---
interface Create {}
interface Update {}

// --- Test DTO class ---
static class UserDto {

@Schema(description = "Primary Key ID", groups = { Update.class })
public Integer id;

@Schema(description = "Full name", groups = { Create.class, Update.class })
public String name;

@Schema(description = "Email address") // no groups -> should remain null
public String email;
}

@Test(description = "groups() on @Schema with a single group should be resolved into the model")
public void testSchemaGroupsSingleGroup() {
final Map<String, io.swagger.v3.oas.models.media.Schema> schemas =
ModelConverters.getInstance().readAll(UserDto.class);
final io.swagger.v3.oas.models.media.Schema model = schemas.get("UserDto");
assertNotNull(model, "UserDto schema must be resolved");

final Map<String, io.swagger.v3.oas.models.media.Schema> properties = model.getProperties();
assertNotNull(properties, "properties must not be null");

final io.swagger.v3.oas.models.media.Schema idSchema = properties.get("id");
assertNotNull(idSchema, "id property must exist");
assertNotNull(idSchema.getGroups(), "id groups must not be null");
assertEquals(idSchema.getGroups().size(), 1);
assertTrue(idSchema.getGroups().contains("Update"), "id should belong to Update group");
}

@Test(description = "groups() on @Schema with multiple groups should all be resolved")
public void testSchemaGroupsMultipleGroups() {
final Map<String, io.swagger.v3.oas.models.media.Schema> schemas =
ModelConverters.getInstance().readAll(UserDto.class);
final io.swagger.v3.oas.models.media.Schema model = schemas.get("UserDto");
final io.swagger.v3.oas.models.media.Schema nameSchema = (io.swagger.v3.oas.models.media.Schema) model.getProperties().get("name");

assertNotNull(nameSchema, "name property must exist");
assertNotNull(nameSchema.getGroups(), "name groups must not be null");

final List<String> groups = nameSchema.getGroups();
assertEquals(groups.size(), 2, "name should have exactly 2 groups");
assertTrue(groups.contains("Create"), "name should belong to Create group");
assertTrue(groups.contains("Update"), "name should belong to Update group");
}

@Test(description = "No groups() on @Schema should leave groups field as null")
public void testSchemaNoGroupsRemainsNull() {
final Map<String, io.swagger.v3.oas.models.media.Schema> schemas =
ModelConverters.getInstance().readAll(UserDto.class);
final io.swagger.v3.oas.models.media.Schema model = schemas.get("UserDto");
final io.swagger.v3.oas.models.media.Schema emailSchema = (io.swagger.v3.oas.models.media.Schema) model.getProperties().get("email");

assertNotNull(emailSchema, "email property must exist");
assertNull(emailSchema.getGroups(),
"email groups should be null when no groups() is specified on @Schema");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,11 @@ public String _const() {
public SchemaResolution schemaResolution() {
return schemaAnnotation.schemaResolution();
}

@Override
public Class<?>[] groups() {
return schemaAnnotation.groups();
}
};

Optional<Schema> schema =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,15 @@ protected void setOperationObjectFromApiOperationAnnotation(
}
}
}

if (apiOperation.groups() != Void.class) {
String groupName = apiOperation.groups().getSimpleName();
if (openapi31) {
operation.addExtension31("x-groups", groupName);
} else {
operation.addExtension("x-groups", groupName);
}
}
}

protected String getOperationId(String operationId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.swagger.v3.jaxrs2;

import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import org.testng.annotations.Test;

import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;

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

/**
* Tests for issue #4928 - Validation Grouping support in @Operation.
* Verifies that groups() in @Operation is serialized as x-groups extension.
*/
public class OperationGroupsTest {

// --- Validation group marker interfaces ---
interface Create {}
interface Update {}

// --- Test JAX-RS resource ---
@Path("/users")
static class UserResource {

@POST
@Operation(summary = "Create user", groups = Create.class)
public void create() {}

@PUT
@Operation(summary = "Update user", groups = Update.class)
public void update() {}

@javax.ws.rs.GET
@Operation(summary = "Get user") // no groups -> no x-groups extension
public void get() {}
}

@Test(description = "@Operation groups() should be written as x-groups extension on POST operation")
public void testOperationGroupsCreate() {
Reader reader = new Reader(new OpenAPI());
OpenAPI openAPI = reader.read(UserResource.class);

assertNotNull(openAPI.getPaths(), "paths must not be null");
PathItem pathItem = openAPI.getPaths().get("/users");
assertNotNull(pathItem, "/users path must exist");
assertNotNull(pathItem.getPost(), "POST operation must exist");

Object groupsExt = pathItem.getPost().getExtensions().get("x-groups");
assertNotNull(groupsExt, "x-groups extension must be set on POST");
assertEquals(groupsExt.toString(), "Create",
"POST should have x-groups=Create");
}

@Test(description = "@Operation groups() should be written as x-groups extension on PUT operation")
public void testOperationGroupsUpdate() {
Reader reader = new Reader(new OpenAPI());
OpenAPI openAPI = reader.read(UserResource.class);

PathItem pathItem = openAPI.getPaths().get("/users");
assertNotNull(pathItem.getPut(), "PUT operation must exist");

Object groupsExt = pathItem.getPut().getExtensions().get("x-groups");
assertNotNull(groupsExt, "x-groups extension must be set on PUT");
assertEquals(groupsExt.toString(), "Update",
"PUT should have x-groups=Update");
}

@Test(description = "No groups() on @Operation should not add x-groups extension")
public void testOperationNoGroupsHasNoExtension() {
Reader reader = new Reader(new OpenAPI());
OpenAPI openAPI = reader.read(UserResource.class);

PathItem pathItem = openAPI.getPaths().get("/users");
assertNotNull(pathItem.getGet(), "GET operation must exist");

// Either no extensions at all, or x-groups not present
if (pathItem.getGet().getExtensions() != null) {
assertNull(pathItem.getGet().getExtensions().get("x-groups"),
"GET should NOT have x-groups when no groups() is set");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ public String toString() {
protected List<T> _enum = null;
private Discriminator discriminator = null;

/**
* @since 2.2.54
*/
private List<String> groups = null;

@JsonIgnore
private boolean exampleSetFlag;
@JsonIgnore
Expand Down Expand Up @@ -2223,6 +2228,32 @@ public Schema extensions(java.util.Map<String, Object> extensions) {
return this;
}

/**
* Returns the validation groups associated with this schema property.
* Group names are stored as simple class name strings (e.g. "Create", "Update").
*
* @since 2.2.54
* @return list of validation group names, or null if not specified
*/
public List<String> getGroups() {
return groups;
}

/**
* @since 2.2.54
*/
public void setGroups(List<String> groups) {
this.groups = groups;
}

/**
* @since 2.2.54
*/
public Schema groups(List<String> groups) {
this.groups = groups;
return this;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Expand Down