diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/annotations/EnumDescription.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/annotations/EnumDescription.java new file mode 100644 index 000000000..aa4fef21e --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/annotations/EnumDescription.java @@ -0,0 +1,51 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 org.springdoc.core.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Scans an Enum class from the annotated field and automatically adds its constants + * and descriptions to the OpenAPI documentation. + * + * @author TAEWOOKK + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnumDescription { + + /** + * The field name in the Enum class to read for description. + * If left empty (default), the library will attempt to find "description" field. + * + * @return The name of the field to read (e.g., "label", "description", "value"). + */ + String fieldName() default ""; +} diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java index 0b3154fcf..c8a7fc07e 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java @@ -75,6 +75,7 @@ import org.springdoc.core.customizers.RouterOperationCustomizer; import org.springdoc.core.customizers.ServerBaseUrlCustomizer; import org.springdoc.core.customizers.SpringDocCustomizers; +import org.springdoc.core.customizers.EnumDescriptionOperationCustomizer; import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer; import org.springdoc.core.events.SpringDocAppInitializer; import org.springdoc.core.extractor.MethodParameterPojoExtractor; @@ -719,6 +720,18 @@ MethodParameterPojoExtractor methodParameterPojoExtractor(SchemaUtils schemaUtil return new MethodParameterPojoExtractor(schemaUtils); } + /** + * Enum description operation customizer. + * + * @return the enum description operation customizer + */ + @Bean + @ConditionalOnMissingBean + @Lazy(false) + GlobalOperationCustomizer enumDescriptionOperationCustomizer() { + return new EnumDescriptionOperationCustomizer(); + } + /** * Spring doc app initializer spring doc app initializer. * diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/EnumDescriptionOperationCustomizer.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/EnumDescriptionOperationCustomizer.java new file mode 100644 index 000000000..5f829afa4 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/EnumDescriptionOperationCustomizer.java @@ -0,0 +1,188 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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 org.springdoc.core.customizers; + +import io.swagger.v3.oas.models.Operation; +import org.apache.commons.lang3.StringUtils; +import org.springdoc.core.annotations.EnumDescription; +import org.springdoc.core.service.AbstractRequestService; +import org.springframework.core.MethodParameter; +import org.springframework.web.method.HandlerMethod; + +import java.lang.reflect.Field; + +/** + * The type Enum description operation customizer. + * Automatically adds enum constants and descriptions to operation. + * when fields are annotated with {@link EnumDescription}. + * + * @author TAEWOOKK + */ +public class EnumDescriptionOperationCustomizer implements GlobalOperationCustomizer{ + + /** + * The constant DEFAULT_FIELD_NAME. + */ + private static final String DEFAULT_FIELD_NAME = "description"; + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + if (operation == null || handlerMethod == null) { + return operation; + } + + for (MethodParameter methodParameter : handlerMethod.getMethodParameters()) { + Class dtoClass = methodParameter.getParameterType(); + + if (!isValidDtoClass(dtoClass)) { + continue; + } + + String enumDescriptions = extractEnumDescriptionsFromDto(dtoClass); + if (StringUtils.isNotBlank(enumDescriptions)) { + String existingDescription = operation.getDescription(); + String newDescription = StringUtils.isNotBlank(existingDescription) + ? existingDescription + "\n\n" + enumDescriptions + : enumDescriptions; + operation.setDescription(newDescription); + } + } + + return operation; + } + + /** + * Is valid dto class boolean. + * + * @param clazz the clazz + * @return the boolean + */ + private boolean isValidDtoClass(Class clazz) { + return !clazz.isPrimitive() + && !clazz.isEnum() + && !clazz.isArray() + && !clazz.isInterface() + && !clazz.getName().startsWith("java.") + && !AbstractRequestService.isRequestTypeToIgnore(clazz); + } + + /** + * Extract enum descriptions from dto string. + * + * @param dtoClass the dto class + * @return the string + */ + private String extractEnumDescriptionsFromDto(Class dtoClass) { + if (dtoClass == null) { + return null; + } + + StringBuilder result = new StringBuilder(); + Class currentClass = dtoClass; + + while (currentClass != null && !currentClass.getName().startsWith("java.")) { + for (Field field : currentClass.getDeclaredFields()) { + EnumDescription annotation = field.getAnnotation(EnumDescription.class); + + if (annotation != null && field.getType().isEnum()) { + String enumDescriptionText = extractEnumDescription( + field.getType(), + annotation.fieldName(), + field.getName() + ); + + if (StringUtils.isNotBlank(enumDescriptionText)) { + if (!result.isEmpty()) { + result.append("\n\n"); + } + result.append(enumDescriptionText); + } + } + } + currentClass = currentClass.getSuperclass(); + } + + return !result.isEmpty() ? result.toString() : null; + } + + /** + * Extract enum description string. + * + * @param enumClass the enum class + * @param fieldName the field name + * @param fieldDisplayName the field display name + * @return the string + */ + private String extractEnumDescription(Class enumClass, + String fieldName, + String fieldDisplayName) { + Object[] enumConstants = enumClass.getEnumConstants(); + if (enumConstants == null || enumConstants.length == 0) { + return null; + } + + StringBuilder sb = new StringBuilder(); + sb.append("**").append(fieldDisplayName).append("**\n"); + + for (Object enumConstant : enumConstants) { + String enumKey = ((Enum) enumConstant).name(); + String description = getEnumDescription(enumConstant, fieldName); + + sb.append("- `").append(enumKey).append("`"); + if (StringUtils.isNotBlank(description)) { + sb.append(": ").append(description); + } + sb.append("\n"); + } + + return sb.toString(); + } + + /** + * Gets enum description string. + * + * @param enumConstant the enum constant + * @param fieldName the field name + * @return the string + */ + private String getEnumDescription(Object enumConstant, String fieldName) { + if (enumConstant == null) { + return ""; + } + + String targetFieldName = StringUtils.isNotBlank(fieldName) ? fieldName : DEFAULT_FIELD_NAME; + + try { + Field field = enumConstant.getClass().getDeclaredField(targetFieldName); + field.setAccessible(true); + Object value = field.get(enumConstant); + return value != null ? value.toString() : ""; + } catch (NoSuchFieldException | IllegalAccessException e) { + return ""; + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/BaseRequest.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/BaseRequest.java new file mode 100644 index 000000000..40503bb18 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/BaseRequest.java @@ -0,0 +1,43 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +import org.springdoc.core.annotations.EnumDescription; + +public class BaseRequest { + + @EnumDescription + private Status status; + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/DeepModel.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/DeepModel.java new file mode 100644 index 000000000..f09e17d2f --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/DeepModel.java @@ -0,0 +1,12 @@ +package test.org.springdoc.api.v30.app244; + +import org.springdoc.core.annotations.EnumDescription; + +public class DeepModel { + @EnumDescription + private Status status; + + public Status getStatus() { + return status; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/HelloController.java index 1c956bfa4..b042b9a3f 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/HelloController.java +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/HelloController.java @@ -17,69 +17,46 @@ * * * * * * 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.app244; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import org.springdoc.core.annotations.ParameterObject; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { - public record Greeting(String hi, String bye) { + @PostMapping("/test") + @Operation(description = "Endpoint to request test resource") + public void test(@RequestBody TestRequest request) { } - @PostMapping(value = "v1/greet", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - public void endpoint(@RequestBody @ModelAttribute Greeting greeting) { - + @GetMapping("/test2") + @Operation(description = "Endpoint to request test2 resource") // ← test2 + public void test2(TestRequest2 request) { } - @PostMapping(value = "v2/greet") - public void endpoint2(@ParameterObject @ModelAttribute Greeting greeting) { - + @GetMapping("/test3") + @Operation(description = "Endpoint to request test3 resource") // ← test3 + public void test3(TestRequest3 request) { } - @PostMapping(value = "v3/greet") - @RequestBody( - content = @Content( - mediaType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - schema = @Schema(implementation = Greeting.class) - )) - public void endpoint3(Greeting greeting) { - + @GetMapping("/test4") + @Operation(description = "Endpoint to request test4 resource") // ← test4 + public void test4(TestRequest4 request) { } - @PostMapping(value = "v4/greet") - public void endpoint4( - @RequestBody(content = @Content( - mediaType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - schema = @Schema(implementation = Greeting.class))) - @ModelAttribute Greeting greeting) { - + @PostMapping("/test5") + @Operation(description = "Endpoint to request test5 resource") + public void test5(@RequestBody TestRequest5 request) { } - - @PostMapping(value = "v5/greet") - @Operation( - requestBody = @RequestBody(content = @Content( - mediaType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - schema = @Schema(implementation = Greeting.class)) - )) - public void endpoint5(@ModelAttribute Greeting greeting) { - - } - -} - +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/Priority.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/Priority.java new file mode 100644 index 000000000..e0afc0da3 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/Priority.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +public enum Priority { + HIGH("High priority"), + LOW("Low priority"); + + private final String label; + + Priority(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/SpringDocApp244Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/SpringDocApp244Test.java index 3c51cc4d1..f3db8cfa4 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/SpringDocApp244Test.java +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/SpringDocApp244Test.java @@ -3,30 +3,33 @@ * * * * * * * * * - * * * * * Copyright 2025 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 + * * * * * * Copyright 2019-2025 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. + * * * * * * * * * * * - * * * * * 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.app244; -import org.springframework.boot.autoconfigure.SpringBootApplication; import test.org.springdoc.api.v30.AbstractSpringDocV30Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; + public class SpringDocApp244Test extends AbstractSpringDocV30Test { @SpringBootApplication diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/Status.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/Status.java new file mode 100644 index 000000000..a58ba166d --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/Status.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +public enum Status { + ACTIVE("Active status"), + INACTIVE("Inactive status"); + + private final String description; + + Status(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest.java new file mode 100644 index 000000000..f80746a4f --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +import org.springdoc.core.annotations.EnumDescription; + +public class TestRequest { + @EnumDescription + private Status status; + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest2.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest2.java new file mode 100644 index 000000000..fd6cc28b4 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest2.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +import org.springdoc.core.annotations.EnumDescription; + +public class TestRequest2 { + @EnumDescription(fieldName = "label") + private Priority priority; + + public Priority getPriority() { + return priority; + } + + public void setPriority(Priority priority) { + this.priority = priority; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest3.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest3.java new file mode 100644 index 000000000..94c7fb4bd --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest3.java @@ -0,0 +1,49 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +import org.springdoc.core.annotations.EnumDescription; + +public class TestRequest3 { + @EnumDescription(fieldName = "label") + private Priority priority; + + @EnumDescription(fieldName = "label") + private Priority priority2; + + public Priority getPriority() { + return priority; + } + + public Priority getPriority2() { + return priority2; + } + + public void setPriority(Priority priority) { + this.priority = priority; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest4.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest4.java new file mode 100644 index 000000000..8f37a19da --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest4.java @@ -0,0 +1,36 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +public class TestRequest4 { + private DeepModel deepModel; + + public DeepModel getDeepModel() { + return deepModel; + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest5.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest5.java new file mode 100644 index 000000000..14085d87d --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app244/TestRequest5.java @@ -0,0 +1,43 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.app244; + +import org.springdoc.core.annotations.EnumDescription; + +public class TestRequest5 extends BaseRequest { + + @EnumDescription(fieldName = "label") + private Priority priority; + + public Priority getPriority() { + return priority; + } + + public void setPriority(Priority priority) { + this.priority = priority; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app244.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app244.json index bbc136c6e..6407a0abe 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app244.json +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app244.json @@ -9,18 +9,20 @@ "description" : "Generated server url" } ], "paths" : { - "/v5/greet" : { + "/test" : { "post" : { "tags" : [ "hello-controller" ], - "operationId" : "endpoint5", + "description" : "Endpoint to request test resource\n\n**status**\n- `ACTIVE`: Active status\n- `INACTIVE`: Inactive status\n", + "operationId" : "test", "requestBody" : { "content" : { - "application/x-www-form-urlencoded" : { + "application/json" : { "schema" : { - "$ref" : "#/components/schemas/Greeting" + "$ref" : "#/components/schemas/TestRequest" } } - } + }, + "required" : true }, "responses" : { "200" : { @@ -29,19 +31,19 @@ } } }, - "/v4/greet" : { - "post" : { + "/test2" : { + "get" : { "tags" : [ "hello-controller" ], - "operationId" : "endpoint4", - "requestBody" : { - "content" : { - "application/x-www-form-urlencoded" : { - "schema" : { - "$ref" : "#/components/schemas/Greeting" - } - } + "description" : "Endpoint to request test2 resource\n\n**priority**\n- `HIGH`: High priority\n- `LOW`: Low priority\n", + "operationId" : "test2", + "parameters" : [ { + "name" : "request", + "in" : "query", + "required" : true, + "schema" : { + "$ref" : "#/components/schemas/TestRequest2" } - }, + } ], "responses" : { "200" : { "description" : "OK" @@ -49,19 +51,19 @@ } } }, - "/v3/greet" : { - "post" : { + "/test3" : { + "get" : { "tags" : [ "hello-controller" ], - "operationId" : "endpoint3", - "requestBody" : { - "content" : { - "application/x-www-form-urlencoded" : { - "schema" : { - "$ref" : "#/components/schemas/Greeting" - } - } + "description" : "Endpoint to request test3 resource\n\n**priority**\n- `HIGH`: High priority\n- `LOW`: Low priority\n\n\n**priority2**\n- `HIGH`: High priority\n- `LOW`: Low priority\n", + "operationId" : "test3", + "parameters" : [ { + "name" : "request", + "in" : "query", + "required" : true, + "schema" : { + "$ref" : "#/components/schemas/TestRequest3" } - }, + } ], "responses" : { "200" : { "description" : "OK" @@ -69,23 +71,17 @@ } } }, - "/v2/greet" : { - "post" : { + "/test4" : { + "get" : { "tags" : [ "hello-controller" ], - "operationId" : "endpoint2", + "description" : "Endpoint to request test4 resource", + "operationId" : "test4", "parameters" : [ { - "name" : "hi", + "name" : "request", "in" : "query", - "required" : false, + "required" : true, "schema" : { - "type" : "string" - } - }, { - "name" : "bye", - "in" : "query", - "required" : false, - "schema" : { - "type" : "string" + "$ref" : "#/components/schemas/TestRequest4" } } ], "responses" : { @@ -95,22 +91,24 @@ } } }, - "/v1/greet" : { - "post" : { - "tags" : [ "hello-controller" ], - "operationId" : "endpoint", - "requestBody" : { - "content" : { - "application/x-www-form-urlencoded" : { - "schema" : { - "$ref" : "#/components/schemas/Greeting" + "/test5": { + "post": { + "tags": ["hello-controller"], + "description": "Endpoint to request test5 resource\n\n**priority**\n- `HIGH`: High priority\n- `LOW`: Low priority\n\n\n**status**\n- `ACTIVE`: Active status\n- `INACTIVE`: Inactive status\n", + "operationId": "test5", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestRequest5" } } - } + }, + "required": true }, - "responses" : { - "200" : { - "description" : "OK" + "responses": { + "200": { + "description": "OK" } } } @@ -118,14 +116,64 @@ }, "components" : { "schemas" : { - "Greeting" : { + "TestRequest" : { + "type" : "object", + "properties" : { + "status" : { + "type" : "string", + "enum" : [ "ACTIVE", "INACTIVE" ] + } + } + }, + "DeepModel" : { + "type" : "object", + "properties" : { + "status" : { + "type" : "string", + "enum" : [ "ACTIVE", "INACTIVE" ] + } + } + }, + "TestRequest2" : { + "type" : "object", + "properties" : { + "priority" : { + "type" : "string", + "enum" : [ "HIGH", "LOW" ] + } + } + }, + "TestRequest3" : { "type" : "object", "properties" : { - "hi" : { - "type" : "string" + "priority" : { + "type" : "string", + "enum" : [ "HIGH", "LOW" ] + }, + "priority2" : { + "type" : "string", + "enum" : [ "HIGH", "LOW" ] + } + } + }, + "TestRequest4" : { + "type" : "object", + "properties" : { + "deepModel" : { + "$ref" : "#/components/schemas/DeepModel" + } + } + }, + "TestRequest5": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["ACTIVE", "INACTIVE"] }, - "bye" : { - "type" : "string" + "priority": { + "type": "string", + "enum": ["HIGH", "LOW"] } } } diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/HelloController.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/HelloController.java new file mode 100644 index 000000000..5b37966ba --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/HelloController.java @@ -0,0 +1,44 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.ui.app36; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @PostMapping("/test") + public void test(@RequestBody TestRequest request) { + } + + @GetMapping("/test2") + public void test2(TestRequest2 request) { + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/Priority.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/Priority.java new file mode 100644 index 000000000..20a46b930 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/Priority.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.ui.app36; + +public enum Priority { + HIGH("High priority"), + LOW("Low priority"); + + private final String label; + + Priority(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } +} diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/SpringDocApp36Test.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/SpringDocApp36Test.java new file mode 100644 index 000000000..74d399ab2 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/SpringDocApp36Test.java @@ -0,0 +1,83 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.ui.app36; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.test.web.servlet.MvcResult; +import test.org.springdoc.ui.AbstractSpringDocTest; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class SpringDocApp36Test extends AbstractSpringDocTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testEnumDescription() throws Exception { + MvcResult result = mockMvc.perform(get("/v3/api-docs")) + .andExpect(status().isOk()) + .andReturn(); + + String content = result.getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(content); + + String description = jsonNode.at("/paths/~1test/post/description").asText(""); + + assertTrue(description.contains("**status**"), "Should contain status enum description"); + assertTrue(description.contains("`ACTIVE`"), "Should contain ACTIVE enum value"); + assertTrue(description.contains("`INACTIVE`"), "Should contain INACTIVE enum value"); + assertTrue(description.contains("Active status"), "Should contain ACTIVE description"); + assertTrue(description.contains("Inactive status"), "Should contain INACTIVE description"); + } + + @Test + void testEnumDescriptionWithCustomFieldName() throws Exception { + MvcResult result = mockMvc.perform(get("/v3/api-docs")) + .andExpect(status().isOk()) + .andReturn(); + + String content = result.getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(content); + + String description = jsonNode.at("/paths/~1test2/get/description").asText(""); + + assertTrue(description.contains("**priority**"), "Should contain priority enum description"); + assertTrue(description.contains("`HIGH`"), "Should contain HIGH enum value"); + assertTrue(description.contains("`LOW`"), "Should contain LOW enum value"); + assertTrue(description.contains("High priority"), "Should contain HIGH label from custom field"); + assertTrue(description.contains("Low priority"), "Should contain LOW label from custom field"); + } + + @SpringBootApplication + static class SpringDocTestApp { + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/Status.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/Status.java new file mode 100644 index 000000000..cb882ede4 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/Status.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.ui.app36; + +public enum Status { + ACTIVE("Active status"), + INACTIVE("Inactive status"); + + private final String description; + + Status(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/TestRequest.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/TestRequest.java new file mode 100644 index 000000000..72a52d9db --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/TestRequest.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.ui.app36; + +import org.springdoc.core.annotations.EnumDescription; + +public class TestRequest { + @EnumDescription + private Status status; + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } +} diff --git a/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/TestRequest2.java b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/TestRequest2.java new file mode 100644 index 000000000..89beaf23e --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/test/java/test/org/springdoc/ui/app36/TestRequest2.java @@ -0,0 +1,42 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2025 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.ui.app36; + +import org.springdoc.core.annotations.EnumDescription; + +public class TestRequest2 { + @EnumDescription(fieldName = "label") + private Priority priority; + + public Priority getPriority() { + return priority; + } + + public void setPriority(Priority priority) { + this.priority = priority; + } +}