1

My question is related to the one posted here.

I had to rephrase my question as I felt the earlier one was too verbose. Having a go at it again!

I have a REST API which returns a list of Assets, and it's coded like this:

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getAllAssets() {
        List<Asset> assets = new ArrayList<Asset>();
        for(int i=1; i<11; i++) {
            assets.add(new Asset(i));
        }

        return RestResponse.create(Status.OK, "10 assets Fetched successfully!", assets);
    }

The response it generates is something like this:

{
    "message":"10 assets Fetched successfully!",
    "content": [{
        "id":"1",
        "type":"LPTP",
        "owner":"Ram",
        "serialNo":"WDKLL3234K3",
        "purchasedOn":"01 Jan 2017"

    },
    {
        "id":"2",
        "type":"LPTP",
        "owner":"Ram",
        "serialNo":"WDKLL3234K3",
        "purchasedOn":"01 Jan 2017"

    },
    ...
    ]
}

I have over 60 services in my application which follow the same response template:

{
        "message":"Message the service wants to send to the client",
        "content": {
            ....
            Entity returned by the service
            ....
        }
}

The following is the RestResponse POJO that represents our Response Template:

 public class RestResponse {
        private String message;
        private Object content;

        public String getMessage() {
            return message;
        }
        public void setMessage(String message) {
            this.message = message;
        }
        public Object getContent() {
            return content;
        }
        public void setContent(Object content) {
            this.content = content;
        }

        private RestResponse(String message, Object content) {
            this.message = message;
            this.content = content;
        }

        public static Response create(Response.Status status, String message, Object content) {
            return Response.status(status).entity(new RestResponse(message, content)).build();
        }
}

Now we're documenting all our APIs using Swagger, and have run into a problem.

Since we're returning the RestResponse class for all our APIs, I wrote the following annotation for my operation:

@ApiOperation(value="Fetches all available assets", response=RestResponse.class, responseContainer="List")

What Swagger does, is in the Definition schema for the RestResponse class looks like this:

"definitions": {
    "RestResponse": {
      "type": "object",
      "properties": {
        "message": {
          "type": "string"
        },
        "content": {
          "type": "object"
        }
      }
    }
 }

Here, I do not get any information or schema about the properties of the object inside the content property.

I understand it's because Swagger doesn't work with Generic Objects.

So if I change my @ApiOperation annotation to the one below:

@ApiOperation(value="Fetches all available assets", response=Asset.class, responseContainer="List")

In the above case, Swagger describes the properties of the Asset entity, but obviously, the message property (of my response template), is missing.

My problem is I want to have both. The content property of my response template can be any entity.

So, can I set response=Asset.class and instruct Swagger to enclose Asset to the RestResponse content property before it documents it? Or any other way in which I can achieve this?

Hope I'm precise this time!

Thanks, Sriram Sridharan.

Edit - After I tried out Marc Nuri's suggestion I created a similar RestServiceResponse.class with a generic object, and set the responseReference attribute for my @ApiOperation to RestServiceResponse<Asset>. And below is the JSON I got.

{
  "swagger": "2.0",
  "info": {
    "version": "1.0.0",
    "title": ""
  },
  "host": "localhost:7070",
  "basePath": "/assets/rest",
  "tags": [
    {
      "name": "Assets"
    }
  ],
  "schemes": [
    "http"
  ],
  "paths": {
    "/assets/{id}": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "Fetches information about a single asset",
        "description": "",
        "operationId": "fetchAssetDetail",
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "type": "integer",
            "format": "int32"
          }
        ],
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "$ref": "#/definitions/RestServiceResponse<Asset>"
            }
          }
        }
      }
    }
  }
}

As you can see, the Definitions section is missing altogether. Am I missing something?

I have my project in GitHub, in case you need to see the whole code. Please help me.

Sriram Sridharan
  • 720
  • 18
  • 43

2 Answers2

0

We are using a similar structure in our microservices responses.

The only difference is that our response container (i.e. RestResponse) has no raw types and uses a Type parameter for the message body/content.

Please try to change your RestResponse to:

public class RestResponse<T> {
    private String message;
    private T content;

    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getContent() {
        return content;
    }
    public void setContent(T content) {
        this.content = content;
    }

    private RestResponse(String message, T content) {
        this.message = message;
        this.content = content;
    }

    public static <T> Response create(Response.Status status, String message, Object content) {
        return Response.status(status).entity(new RestResponse(message, content)).build();
    }
}

Then annotate your endpoints like this:

 @ApiOperation(
            value="Fetches all available assets",
            responseReference = "RestResponse<List<Asset>>")

This is working OK for the latest version of swagger.

Marc Nuri
  • 2,729
  • 1
  • 14
  • 18
  • Hi @MarcNuri, I tried out your suggestion, but still facing more or less the same problem. Could you please help me out? I have edited the question with details. – Sriram Sridharan Nov 21 '17 at 05:06
  • Hi. In our projects we are using Spring and Swagger2 with swagger-ui. Maybe it has a different behavior. What I described in the solution is not documented in Swagger's library documentation but is working. I'll try to check with your code to see if I can find what's going on. – Marc Nuri Nov 21 '17 at 08:21
0

Thanks again, Marc Nuri!

I may have found a solution that just works for my scenario. Here's what I did. I have my RestServiceResponse class as the same as the one mentioned in my question (the most recent edit), and I just modified by AssetService class by adding a static final class like this:

private static final class AssetResponse extends RestServiceResponse<Asset> {
        private Asset data;
}

Once this is done, I changed my @ApiOperation annotation to this:

@ApiOperation(value="Fetches an asset by ID", produces="application/json", response=AssetResponse.class)

Now, what this basically does is, for pure documentation purposes, it seems to just replace the Generic Type in the RestServiceResponse class with a specific Asset type during compile time, so that Swagger can define the object.

Now when I ran the swagger JSON URL, I got a perfect documentation!

{
  "swagger": "2.0",
  "info": {
    "version": "1.0.0",
    "title": ""
  },
  "host": "localhost:7070",
  "basePath": "/assets/rest",
  "tags": [
    {
      "name": "Assets"
    }
  ],
  "schemes": [
    "http"
  ],
  "paths": {
    "/assets/{id}": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "Fetches information about a single asset",
        "description": "",
        "operationId": "fetchAssetDetail",
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "type": "integer",
            "format": "int32"
          }
        ],
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "$ref": "#/definitions/AssetResponse"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Asset": {
      "type": "object",
      "required": [
        "name",
        "owner",
        "purchaseDate",
        "type"
      ],
      "properties": {
        "id": {
          "type": "integer",
          "format": "int32",
          "description": "The internal unique ID of the Asset"
        },
        "name": {
          "type": "string",
          "description": "Name of the asset"
        },
        "type": {
          "type": "string",
          "description": "Name of the asset",
          "enum": [
            "Laptop",
            "Desktop",
            "Internet Dongle",
            "Tablet",
            "Smartphone"
          ]
        },
        "owner": {
          "type": "string",
          "description": "ID of the person who owns this asset"
        },
        "purchaseDate": {
          "type": "string",
          "format": "date",
          "description": "Date when this asset was purchased"
        }
      }
    },
    "AssetResponse": {
      "type": "object",
      "properties": {
        "message": {
          "type": "string"
        },
        "content": {
          "$ref": "#/definitions/Asset"
        }
      }
    }
  }
}

I don't know yet if this is the most efficient solution possible, but right now it's good enough for me to get the ball rolling. Please share your ideas.

Sriram Sridharan
  • 720
  • 18
  • 43