swagger-api / swagger-core

Examples and server integrations for generating the Swagger API Specification, which enables easy access to your REST API
http://swagger.io
Apache License 2.0
7.37k stars 2.17k forks source link

Swagger does not serializes Generic Objects in API response completely. #3496

Open Tsingh315 opened 4 years ago

Tsingh315 commented 4 years ago

We have over 200 APIs on Jersey (Non-Spring tech stack). We have integrated swagger and are now writing annotations. The entities/pojos returned by our APIs are all wrapped inside a common structure using Generics.

public ApiCallResponse<User> users(){...} public ApiCallResponse<Address> addresses {...} public ApiCallResponse<Liability> addresses {...}

The swagger auto scan only detects ApiCallResponse object. It does not serializes User, Address or Liability for serialization.

I am working around this by creating separate classes for each of them as follows.

@Schema public class ApiCallResponseUser extends ApiCallResponse<User> { } @Schema public class ApiCallResponseAddress extends ApiCallResponse<Address> { } @Schema public class ApiCallResponseAddress extends ApiCallResponse<Liability> { } and use them as follows @ApiResponse( responseCode = "200", description = " Blah.", content = @Content(mediaType = "application/json", schema = @Schema( name = "Blah", implementation = ApiCallResponseUser.class ) ) ),

It would be really nice, if swagger while auto scanning, auto creates those classes internally and includes them in the generated json. This will save me from creating so many classes and configuring them to exclude from code coverage.

ghost commented 4 years ago

Shouldn't you set the implementation property to inform swagger of what the returned entity type is?

@ApiResponse(content = @Content(schema = @Schema(implementation = User.class))
public ApiResponse<User> users(){...}
Tsingh315 commented 4 years ago

My apologies for writing ApiResponse in my first comment. I have changed 'ApiResponse' to 'ApiCallResponse' above. It is actually ApiCallResponse, which is our own custom response wrapper that looks like below.

class ApiCallResponse<E> implements Serializable{
      ...
     @Schema(required = true, description = "Revision number")
     @JsonProperty("revision")
      private int revision

      @Schema(required = true, description = "Timestamp of request")
      @JsonProperty("timestamp")
      private long timestamp;

     @Schema(required = true, description = "Parameterized Result")
     @JsonProperty("result")
     private E result;

}

My return entity type is User but User wrapped inside ApiCallResponse (our custom response wrapper)

   ApiCallResponse<User>

All of my APIs return entities wrapped inside this custom object. We want to retain this wrapper to ensure accuracy to our API consumers.

juanjelas commented 4 years ago

I am in the same situation, my endpoint response returns the wrapper with my object

@ApiResponses(value = {@ApiResponse(description = "Something", responseCode = "200",
      content = {@Content(schema = @Schema(implementation = ResponseWrapper<CardsLimits>.class))})})
  @GetMapping(value = "/card-limits")
  public ResponseWrapper<CardsLimits> getLimits() {}

How can I express that implementation:

 @Schema(implementation = ResponseWrapper<CardsLimits>.class)

Thanks for your time

hehongyu1995 commented 4 years ago

I am in the same situation, my endpoint response returns the wrapper with my object

@ApiResponses(value = {@ApiResponse(description = "Something", responseCode = "200",
      content = {@Content(schema = @Schema(implementation = ResponseWrapper<CardsLimits>.class))})})
  @GetMapping(value = "/card-limits")
  public ResponseWrapper<CardsLimits> getLimits() {}

How can I express that implementation:

 @Schema(implementation = ResponseWrapper<CardsLimits>.class)

Thanks for your time

ResponseWrapper<>.class is not possible in Java...

My question is, Generic response type can be handled properly by swagger2 (springfox), why it become a problem for Swagger3?

I have to use subclass to replace all raw uses of generic wrapper class when using it as api response...It's quite verbose and painful to refactor all controllers.

Hope this problem can be solved as soon as possible :)

bogomolov-a-a commented 4 years ago

In my current work, I extend io.swagger.v3.jaxrs2.Reader class and using wrapper for ResponseWrapper<WrappedClass>. I Found in my service all methods with ResponseWrapper<T> and write ResponseWrapper'T-classname' schema into swagger doc. I can to give a code snippet.

Tsingh315 commented 4 years ago

@bogomolov-a-a Yes, Can you please share your code snippet here? Thanks.

bogomolov-a-a commented 4 years ago

I try this code(from my current service bus project with 20+ REST API without Spring, only resteasy-jaxrs implementation) :

public class GeneralOpenApiReader
        extends io.swagger.v3.jaxrs2.Reader
@Override
    public OpenAPI read(Set<Class<?>> classes) {
/*read openapi as is.*/
        OpenAPI result = super.read(classes);
/*preparing schemas(for updating in generic resolving process)*/
        Map<String, Schema> schemaMap = OpenApiUtil.getSchemasMap(result);
/*for every path(i.e. one service class) on my application I  get operation set(with sorting by custom Comparator)*/
        result.getPaths()
              .forEach((name, pathItem) -> {
                  Set<OperationInfo> sortedOperations = OpenApiUtil.getSortedOperationsFromPath(
                          pathItem);
/*I use given classes set and path name for found real class(in my case only one class by name but can be found any class with `@Path` annotation in current reading context )*/
                  Class<?> pathClass = findClassByPath(classes,
                          name);
/*for any operation I check following cases:
1. generic request body parameter */
                  sortedOperations.forEach(operationInfo -> {
                      Operation operation = operationInfo.getOperation();
                      String operationId = operation.getOperationId();
                      Method method = findMethodByOperationId(
                              pathClass,
                              operationId);
                      handleRequestBody(pathClass, operation, operationId, method);
                      handleResponse(pathClass, operation, operationId, method);
                  });
              });
/*other code*/
..............
        return result;
    }
.................
private Method findMethodByOperationId(
            Class<?> pathClass,
            String operationId) {
        String operationName = OpenApiUtil.getOperationNameById(operationId);
        return getRealResourceMethod(
                Arrays.stream(pathClass.getMethods())
                      .filter(method ->
                      {
                          log.info(method.toGenericString());
                          io.swagger.v3.oas.annotations.Operation operation = method
                                  .getAnnotation(io.swagger.v3.oas.annotations.Operation.class);
                          boolean result = false;
                          if (operation != null) {
                              String declaredOperationId = OpenApiUtil.getOperationNameById(operation.operationId());
                              result = declaredOperationId.equals(operationName);
                          }
                          return (result || method.getName().equals(operationName));
                      })
                      .findFirst()
                      .orElseThrow(() -> new IllegalStateException("not found operation with id '" + operationId + "'")));
    }
..................
private Class<?> findClassByPath(
            Set<Class<?>> classes,
            String name) {

        if (classes.size() == 1) {
            Class<?> result = classes.stream().findFirst().get();
            log.info("found class with name " + result.getName());
            return result;
        }
        return classes
                .stream()
                .filter(x -> {
                    Path path = x.getAnnotation(Path.class);
                    return path.value().equals(name);
                })
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("class not found in list with path'" + name + "'"));
    }
/* stroring info about generic operations. ATTENTION! Swagger allows only 2 parameters(whatever, Header,Query... and other) for invoking this method and resolving operation. Parameters omitted. */
@Override
    public Operation parseMethod(.....) {
        Operation result = super.parseMethod(.....);
        Method realMethod = getRealResourceMethod(method);
        String operationId = result.getOperationId();
        if (isGenericReturnTypeMethod(realMethod)) {
            log.info("found operation with id '" + operationId + "' has generic return type");
            genericResponseOperationIdSet.add(operationId);
        }
        if (isGenericRequestBodyParameterMethod(realMethod)) {
            log.info("found operation with id '" + operationId + "' has generic request body parameter");
            genericRequestBodyOperationIdSet.add(operationId);
        }
        return result;
    }
/detect my parameterized request body.*/
    private boolean isGenericRequestBodyParameterMethod(Method realMethod) {
        return Arrays.stream(realMethod.getParameters())
                     .anyMatch(x ->
                             x.isAnnotationPresent(io.swagger.v3.oas.annotations.parameters.RequestBody.class) &&
                                     x.getParameterizedType() instanceof TypeVariable);
    }
/*detect my ResponseEntity<T> - ResponseWrapper<T> return type for method*/
 private boolean isGenericReturnTypeMethod(Method method) {
        Class<?> returnType = method.getReturnType();
/*in my case need multipart/form-data parametrized entity return from method.*/
        if (!ResponseEntity.class.equals(returnType) && !MultipartFormType.class.isAssignableFrom(returnType)) {
            return false;
        }
        ParameterizedType genericReturnType = (ParameterizedType) method.getGenericReturnType();
        Type[] typeArguments = genericReturnType.getActualTypeArguments();
        Type operationReturnType = typeArguments[0];
        return operationReturnType instanceof TypeVariable;
    }
 private void updateGoodResponse(
            ApiResponse resultGoodResponse,
            RealTypeResult realTypeResult) {

        Content content = resultGoodResponse.getContent();
        /*We believe that in such cases, one data type will always be returned in one content-type.*/
        String contentTypeKey = content.keySet().stream().findFirst().get();
        MediaType oneContentMediaType = content.get(contentTypeKey);
        Schema refSchema = buildRefSchema(realTypeResult);
        oneContentMediaType.setSchema(refSchema);
    }
private RealTypeResult getRealReturnType(
            Method method,
            Class<?> realClass) {
        ParameterizedType operationReturnType = (ParameterizedType) method.getGenericReturnType();
        Class<?> declaringClass = method.getDeclaringClass();
        Map<String, Type> resolvedTypeVariableMap = resolveActualTypeArgs(realClass,
                declaringClass);
        log.info("resolved type variable map:" + Objects.requireNonNull(resolvedTypeVariableMap).toString());
        String typeVariableName = operationReturnType.getActualTypeArguments()[0].getTypeName();
        log.info("type variable name:" + typeVariableName);
        Class<?> result = (Class<?>) resolvedTypeVariableMap.get(typeVariableName);
        log.info("result type:" + result.toString());
        Type returnRawType = operationReturnType.getRawType();
        boolean isMultiPart = returnRawType instanceof Class && MultipartFormType.class.isAssignableFrom((Class<?>) returnRawType);
        return new RealTypeResult(result, isMultiPart, GeneralUtils.getRawAnnotatedType(returnRawType).getSimpleName());
    }

You can found resolveActualTypeArgsv method by https://stackoverflow.com/questions/17297308/how-do-i-resolve-the-actual-type-for-a-generic-return-type-using-reflection I use that.

Building schema name and schema for my real return type:

private Schema buildRefSchema(RealTypeResult realTypeResult) {

        Class<?> realClazz = realTypeResult.clazz;
        Map<String, Schema> schemas = converters.readAll(realClazz);
        getOpenAPI()
                .getComponents()
                .getSchemas()
                .putAll(schemas);
        Schema result = new Schema();
        String schemaName = getRefSchemaName(realTypeResult);
        result.set$ref(RefUtils.constructRef(schemaName));
        return result;
    }

    private String getRefSchemaName(RealTypeResult typeResult) {
        if (typeResult.isMultiPart) {
            return typeResult.multipartClassName + typeResult.clazz.getSimpleName();
        }
        return ResponseEntity.class.getSimpleName() + typeResult.clazz.getSimpleName();
    }

Service code:

public class Service<Input,Output>
{
 public String stub1(String header1, T requestBody)
{
.....
}
@ApiResponse(responseCode="200", content=@Content(schema=@Schema(implemenation=ResponseEntity.class)))
public ResponseEntity<Output> getResponseStub2(String header1,String header2/*ONLY 2(!) parameters*/)
{
....
}
}

For wrapping resteasy framework I develop custom MessageBodyReader and MessageBodyWriter. I hope my snippets help you in your projects! :) If you have any questions, I will be glad to answer.

Sorry for my bad English.

Tsingh315 commented 4 years ago

Thanks @bogomolov-a-a Great effort. Really good documentation.

bogomolov-a-a commented 4 years ago

I don't found another way for parameterizing my API. I found only one pull request there, but it don't merged into master branch.

may1-xiao commented 2 years ago

https://nartc.me/blog/nestjs-swagger-generics

I found this answer, and it works for me.