Search code examples
javaspringspring-bootswaggeropenapi

Spring Boot Open API 3.0 - How to show custom examples without writing a Json Manually


I am working in a Spring Boot 2.5.2 + Java 8 project which has Open API 3.0:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.14</version>
</dependency>

I need to show some specific error bodies to the user like this:

This example comes from another Spring boot project where swagger is mapped with an yaml file. enter image description here




I need to show examples with a similar body but using annotations (because if a new endpoint is added it will be automatically mapped to the Swagger UI. We are facing an issue where developers are forgetting to map the endpoint in the yaml file, this action is trying to avoid this).

Here are my mapping:

@ApiResponse(responseCode = "401",
                    description = "Something is required.",
                    content = {
                            @Content(mediaType = "application/json",

                                    examples = {

                                        //The problem lays here!!
                                    }

                            )

                    })
    })
    public abstract ResponseEntity<Mono<Object>> queryLegacy(String query);

My error class is ErpResponseBodyDTO.class.

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErpResponseBodyDTO{
  private Long code;
  private String message;
}

How can I create a Instance of this class like new ErpResponseBodyDTO(123, "Error Message"); and use this instance as an Example?

I mean, something like:

...
@Content(mediaType = "application/json",

examples = {

    new ErpResponseBodyDTO(123, "Error Message"),
    new ErpResponseBodyDTO(456, "Error Message 2"),
    new ErpResponseBodyDTO(789, "Error Message 3")
}
)
...

I don't want to write manually a Json Example because my API has a lot of possible errors and writing it in the @ExampleObject(value=" {"A":"BIG", "JSON":"HERE"} ") will pollute the code.


Solution

  • SOLVED

    Solution: Document the API programmatically.

    Spring has a lib that you can manipulate the entire OpenAPI project.

    There's my configuration class, which is the solution:

    package br.orchestratorapi.config;
    
    import br.com.dto.ErpResponseBodyDTO;
    import br.com.exception.MessageError;
    import br.com.exception.MyExceptionWrapperClassException;
    import com.google.gson.Gson;
    import com.google.gson.JsonArray;
    import com.google.gson.JsonObject;
    import io.swagger.v3.oas.models.OpenAPI;
    import io.swagger.v3.oas.models.Operation;
    import io.swagger.v3.oas.models.examples.Example;
    import io.swagger.v3.oas.models.media.Content;
    import io.swagger.v3.oas.models.media.MediaType;
    import io.swagger.v3.oas.models.responses.ApiResponse;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import org.springdoc.core.customizers.OpenApiCustomiser;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.Resource;
    import org.springframework.util.FileCopyUtils;
    
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.Reader;
    import java.io.UncheckedIOException;
    import java.util.List;
    import java.util.ArrayList;
    import java.util.Optional;
    
    @Configuration
    public class OpenAPIExamples {
    
    @Value("classpath:/swagger_examples.json")
        private Resource swaggerExamples;
    
        @Bean
        public OpenApiCustomiser openApiCustomiser() {
            return openAPI -> buildExamplesFromJson().forEach(exampleValue -> setExamplesOnEndpoints(openAPI, exampleValue, "application/json"));
        }
    
        private void setExamplesOnEndpoints(OpenAPI api, ExampleValue exampleValue, String bodyType) {
            Optional.ofNullable(api.getPaths())
                    .map(paths -> paths.get(exampleValue.getPath()))
                    .map(paths -> getOperationForHttpMethod(exampleValue.getHttpMethod(), paths))
                    .ifPresent(operation -> setExamplesOnEndpoints(exampleValue, bodyType, operation));
        }
    
        private void setExamplesOnEndpoints(ExampleValue exampleValue, String bodyType, Operation operation) {
            ApiResponse response = operation.getResponses().get(String.valueOf(exampleValue.getHttpStatus()));
            if(response == null)
                 response = new ApiResponse();
    
            Content content = response.getContent();
            if(content == null)
                 content = new Content();
    
            MediaType mediaType = content.get(bodyType);
    
            if(mediaType == null)
                mediaType = new MediaType();
    
            mediaType.addExamples(exampleValue.getExampleName(), exampleValue.getExample());
    
            content.put(bodyType, mediaType);
            response.setContent(content);
    
            operation.getResponses().put(String.valueOf(exampleValue.getHttpStatus()), response);
        }
    
    
        private Operation getOperationForHttpMethod(String httpMethod, PathItem paths) {
            switch (httpMethod) {
                case "POST":
                    return paths.getPost();
                case "PATCH":
                    return paths.getPatch();
                case "PUT":
                    return paths.getPut();
                case "DELETE":
                    return paths.getDelete();
                case "GET":
                    return paths.getGet();
                case "OPTIONS":
                    return paths.getOptions();
                case "HEAD":
                    return paths.getHead();
                case "TRACE":
                    return paths.getTrace();
                default:
                    return null;
            }
        }
    
        public List<ExampleValue> buildExamplesFromJson() {
            List<ExampleValue> result = new ArrayList<>();
    
            JsonObject jsonObject = new Gson().fromJson(asString(swaggerExamples), JsonObject.class);
            JsonArray endpoints = jsonObject.getAsJsonArray("endpoints");
    
            endpoints.forEach(element -> {
                JsonObject path = element.getAsJsonObject();
    
                String endpointPath = path.get("path").getAsString();
                String method = path.get("method").getAsString();
    
                path.get("responses").getAsJsonArray().forEach(response -> {
                    JsonObject resp = response.getAsJsonObject();
    
                    int status = resp.get("status").getAsInt();
    
                    resp.getAsJsonArray("errors").forEach(error -> {
    
                        JsonObject errorObj = error.getAsJsonObject();
                        String enumName = errorObj.get("enum").getAsString();
    
    
                        Example example = new Example();
                        example.description(errorObj.get("description").getAsString());
                        example.value(ErpResponseBodyDTO.builder().error(new MyExceptionWrapperClassException(MessageError.valueOf(enumName)).getResponse().getError()).build());
    
                        ExampleValue exampleValue = new ExampleValue(errorObj.get("exampleName").getAsString(), example, status, method, endpointPath);
    
                        result.add(exampleValue);
    
                    });
    
                });
    
    
            });
    
            return result;
    
        }
    
        public static String asString(Resource resource) {
            try (Reader reader = new InputStreamReader(resource.getInputStream())) {
                return FileCopyUtils.copyToString(reader);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    
        @Data
        @AllArgsConstructor
        private class ExampleValue {
            private String exampleName;
            private Example example;
            private int httpStatus;
            private String httpMethod;
            private String path;
        }
    
     }
    
    

    As you can see, I read a Json file from resources:

    {
      "endpoints": [
    
    
        {
          "path":  "/endpoint/that/{already}/{exists-in-swagger}",
          "method": "POST",
          "responses": [
    
            {
              "status":  401,
              "errors":[
                {"description":"A description","enum": "ENUM_THAT_I_HAVE_SO_PAY_ATTENTION", "exampleName":  "You have thrown an error"}
              ]
            }
    
          ]
        }
    
    
      ]
    }