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

JAX-RS BeanParam issue with multipart/formdata and file upload #3092

Closed markusdemetz closed 4 years ago

markusdemetz commented 5 years ago

I try to put the form parameters of a multipart request into a bean like this:

@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response uploadWithBean(@Parameter @BeanParam UploadRequest personData) {
    InputStream upload = personData.getUpload();
    if (upload == null) {
        return Response.ok(UploadResponse.FAILED).build();
    }
    return Response.ok(UploadResponse.SUCCESS).build();
}

In this case, the JSON produced ist the following:

"schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "picture": {
                    "$ref": "#/components/schemas/FormDataContentDisposition"
                  }
                }
              }

instead of (without the @BeanParam annotation).

"schema": {
                "$ref": "#/components/schemas/UploadRequest"
}

If I leave the @BeanParam annotation, i would get a 415 response code, so this seems to be the right way.

If I further annotate the file-parameter InputStream of my bean, the upload will not occur and InputStream is null.

@Schema(name = "UploadRequest", title="Schema for Upload")
public class UploadRequest  {
    @FormDataParam("name")
    // @Schema(name="name", type="string", accessMode=AccessMode.READ_WRITE)
    private String name;

    @FormDataParam("picture")
    @Schema(name="picture", type="string", format="binary") // <----- upload is NULL when @Schema is present
    private InputStream upload;

    @FormDataParam("picture")
    private FormDataContentDisposition disposition;
}

See the provided example to test. swagger-test-upload.zip

patryk-collibra commented 4 years ago

+1 Without handling it properly, generated Open API clients are useless for complex requests including file upload.

frantuma commented 4 years ago

there are 2 separated issues:

  1. If I further annotate the file-parameter InputStream of my bean, the upload will not occur and InputStream is null.

this is unrelated to swagger-core integration but caused by any annotation alongside @FormDataParam (defined under @FormDataParam); see #3009 and #2567, and root cause in jersey: https://github.com/jersey/jersey/issues/3458

Workaround is switching the annotations like:

    @Schema(name="picture", type="string", format="binary")
    @FormDataParam("picture")
    private InputStream upload;
  1. the resolved requestBody in the returned OpenAPI definition

Up to 2.1.2 the best solution for your scenario is annotating the response with:

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.APPLICATION_JSON)
    @RequestBody(content = @Content(schema = @Schema(implementation = UploadRequest.class)))
    public Response uploadWithBean(@BeanParam UploadRequest personData) {

and the bean with:

@Schema(name = "UploadRequest", title="Schema for Upload")
public class UploadRequest  {

    @FormDataParam("name")
    private String name;

    @Schema(name="picture", type="string", format="binary")
    @FormDataParam("picture")
    private InputStream upload;

    @Hidden
    @FormDataParam("picture")
    private FormDataContentDisposition disposition;

3580 (available in next release 2.1.3) fixes the handling of hidden annotations with @BeanParam, therefore the @RequestBody annotation on the resource method will not be needed.

Please close ticket if this answers your question

markusdemetz commented 4 years ago

That solves the issue. Thank You!!

markusdemetz commented 4 years ago

3580 (available in next release 2.1.3) fixes the handling of hidden annotations with @BeanParam, therefore the @RequestBody annotation on the resource method will not be needed.

The @RequestBody is still needed to work properly (using 2.1.4). When the mapping ist annotated, it produces the following JSON: annotated

Without annotation, I get: not-annotated

The former is correct, but the "upload"-schema is useless, or am I wrong? Provided example is attached.

swagger-test-upload2.zip

eak24 commented 3 years ago

I also see that @RequestBody is still needed (and I am on the current latest stable at 2.1.5)

eak24 commented 3 years ago

Specifically, I've been driven crazy by this comment in DefaultParameterExtension: // Re-process all Bean fields and let the default swagger-jaxrs/swagger-jersey-jaxrs processors do their thing because it suggests there should be some additional parameter extension to do that processing, yet I only had the Default one installed. I went down that hole and installed swagger-jeresy-jaxrs:1.6.2 but that too didn't work. So I'm left wondering - is there some parameter extension that I am missing? And if so, where is it? I have the latest swagger core and swagger jaxrs installed. Any help/suggestions would be great!

frantuma commented 3 years ago

I am not sure about the specific issue here, maybe worth fully describing along with test case in new ticket; since 2.1.3 @RequestBody is not needed any more, and example attached to above comment seems to demonstrate that, output being:

{
   "openapi":"3.0.1",
   "info":{
      "title":"Test",
      "version":"v1"
   },
   "paths":{
      "/api/v1/upload":{
         "post":{
            "operationId":"uploadWithBean",
            "requestBody":{
               "content":{
                  "multipart/form-data":{
                     "schema":{
                        "type":"object",
                        "properties":{
                           "name":{
                              "type":"string"
                           },
                           "upload":{
                              "$ref":"#/components/schemas/upload"
                           }
                        }
                     }
                  }
               }
            },
            "responses":{
               "default":{
                  "description":"default response",
                  "content":{
                     "application/json":{

                     }
                  }
               }
            }
         }
      },
   "components":{
      "schemas":{
         "upload":{
            "type":"object",
            "format":"binary"
         }
      }
   }
}

applying @RequestBody still works (but is not needed), but as mentioned has a side effect of keeping around an unused component/schemas/upload model which is not used (could be addressed in separate ticket).

about // Re-process all Bean fields and let the default swagger-jaxrs/swagger-jersey-jaxrs processors do their thing this is indeed rather obscure, and due do historical reasons, but it doesn't imply any other processor. Again I am not sure what the current issue is, if still experiencing issues please open a new ticket with test case and reference this ticket.

eak24 commented 3 years ago

Turns out that the issue was that we were using private fields with an underscore at the end, and so some part of Jackson/Swagger core wasn't able to apply the annotations (pathparam, queryparam, etc...) to the interpreted field from the public getter/setter like so:

public class MyClass {

    @PathParam
    private String privateField_;

    private String getPrivateField() {
        return privateField_;
    }
}

So in this example, Swagger core didn't see that privateField was an accessible PathParam. I'm fine with this limitation, and I see there is no simple, robust way for Swagger core to correlate the field with the method, but I'm still a little confused because the beanParam resolution does happen correctly on the server - ie, Spring is able to properly inject the BeanParam, so why can't Swagger core use the logic that Spring uses?