Spring boot OpenAPI 3 download multipart with JSON content and attachment - spring-boot

I am seeing a 406 error when I try to download a file with Swagger 3.0 schema definition. The schema definition for a multipart file download which looks something like the below.
"get": {
"operationId": "getAttachment",
"summary": "Retrieve attachments to a existing Ticket",
"tags": [
"changeRequest"
],
"parameters": [
{
"required": true,
"name": "id",
"in": "path",
"description": "Identifier of the Change Request",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Ok",
"headers": {
},
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"metadata": {
"$ref": "#/components/schemas/Attachment"
},
"file": {
"type": "string",
"format":"binary",
"description": "Actual File Attachment"
}
}
}
}
}
}
}
}
It generates the following class when built using the swagger plugin which seems to be apt:
public class InlineResponse200 {
#JsonProperty("metadata")
private Attachment metadata = null;
#JsonProperty("file")
private Resource file;
Following is the implementation generated:
#ApiOperation(value = "Retrieve attachments to a existing Ticket", nickname = "getAttachment", notes
= "", response = InlineResponse200.class, tags={ "changeRequest", })
#RequestMapping(value = "/changeRequest/attachment/{id}",
produces = { "multipart/form-data", "application/json" },
method = RequestMethod.GET)
public ResponseEntity<InlineResponse200> getAttachment(#PathVariable("id") String id) {
Attachment lAttachmentMetadata = new Attachment();
lAttachmentMetadata.setDescription("This is a sample description");
lAttachmentMetadata.setSize(2000);
FileSystemResource fileSysResource = new FileSystemResource(new File("C:\\Projects\\Service Assurance\\Chnage Mgmt\\Attachments\\attachment.txt"));
InlineResponse200 responseObject = new InlineResponse200();
responseObject.setFile(fileSysResource);
responseObject.setMetadata(lAttachmentMetadata);
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileSysResource.getFilename() + "\"").header("Content-Type", "multipart/form-data").body(responseObject);
}
When I invoke the service I see a 406 error returned
Thu Oct 15 03:46:39 IST 2020:DEBUG:<< "{"timestamp":"2020-10-14T22:16:39.258Z","status":406,"error":"Not Acceptable","message":"Could not find acceptable representation","path":"/changeManagement/api/v1/changeRequest/attachment/1234"}"
SOAPUI REST SERVICE TEST
Any help or pointers in the right direction would be much appreciated.

Related

Spring boot swagger multipart with json content and attachment

I've a requirement to upload any file content using Swagger-3 along with some metadata information as a JSON within a single request. Therefore I configured following into my swagger:
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"metadata": {
"$ref": "#/components/schemas/Attachment"
},
"file": {
"type": "string",
"format":"binary",
"description": "Actual File Attachment"
}
}
}
}
},
"description": "The Attachment record / entry be created",
"required": true
}
It translates to following when I build the controller object:
#ApiOperation(value = "Upload attachments to a existing Ticket", nickname = "uploadAttachment", notes = "", response = Attachment.class, responseContainer = "List", tags={ "changeRequest", })
#RequestMapping(value = "/changeRequest/attachment/{id}",
produces = { "application/json" },
consumes = { "multipart/form-data" },
method = RequestMethod.POST)
public ResponseEntity<List<Attachment>> uploadAttachment(#ApiParam(value = "Identifier of the Change Request",required=true) #PathVariable("id") String id,#ApiParam(value = "Application ID invoking the call" ,required=true) #RequestHeader(value="X-App-Id", required=true) String xAppId,#NotNull #ApiParam(value = "To track unique transaction across multiple systems for audit trail", required = true) #Valid #RequestParam(value = "X-Transaction-Id", required = true) String xTransactionId,#ApiParam(value = "Authorization header" ) #RequestHeader(value="authorization", required=false) String authorization,#ApiParam(value = "", defaultValue="null") #RequestParam(value="metadata", required=false) Attachment metadata, #ApiParam(value = "file detail") #Valid #RequestPart("file") MultipartFile file) {
ResponseEntity<List<Attachment>> responseEntity = new ResponseEntity<>(HttpStatus.OK);
responseEntity.getBody().add(metadata);
return responseEntity;
}
Following is the Attachment schema definition:
"Attachment": {
"type": "object",
"description": "Attachment Metadata definition",
"properties": {
"description": {
"type": "string",
"description": "A narrative text describing the content of the attachment"
},
"href": {
"type": "string",
"description": "Reference of the attachment"
},
"id": {
"type": "string",
"description": "Unique identifier of the attachment"
},
"mimeType": {
"type": "string",
"description": "The mime type of the document as defined in RFC 2045 and RFC 2046 specifications."
},
"name": {
"type": "string",
"description": "The name of the file"
},
"path": {
"type": "string",
"description": "The path of the attached file"
},
"size": {
"type": "integer",
"description": "The size of the file (sizeUnit if present indicates the unit, otherwise kilobytes is the default)."
},
"sizeUnit": {
"type": "integer",
"description": "The unit size for expressing the size of the file (MB,kB...)"
},
"url": {
"type": "string",
"description": "Uniform Resource Locator, is a web page address (a subset of URI)"
},
"validFor": {
"$ref": "#/components/schemas/TimePeriod",
"description": "Period of validity of the attachment"
},
"#type": {
"type": "string",
"description": "The class type of the actual resource (for type extension)."
},
"#schemaLocation": {
"type": "string",
"description": "A link to the schema describing a resource (for type extension)."
},
"#baseType": {
"type": "string",
"description": "The base type for use in polymorphic collections"
}
}
}
In the example above, Attachment metadata is what I am trying to pass as part of the SOAP API test. However I keep getting following error:
Mon Oct 12 17:06:28 IST 2020:DEBUG:<< "{"timestamp":"2020-10-12T11:36:28.371Z","status":500,"error":"Internal Server Error","message":"Failed to convert value of type 'java.lang.String' to required type 'com.bell.na.nt.change.swagger.model.Attachment'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.bell.na.nt.change.swagger.model.Attachment': no matching editors or conversion strategy found","path":"/changeManagement/api/v1/changeRequest/attachment/1234"}"
Why is the string not being converted and mapped to the JSON object. Not sure I am missing anything. Following is what my json looks like.
{"#baseType": "string", "#schemaLocation": "string", "#type": "string", "description": "string", "href": "string", "id": "string", "mimeType": "string", "name": "string", "path": "string", "size": 0, "sizeUnit": 0, "url": "string", "validFor": { "endDateTime": "2020-10-11T19:06:40.586Z", "startDateTime": "2020-10-11T19:06:40.586Z"}}
Postman Request
Turns out I had to add the a converter for converting the string representation of the JSON to desired Swagger generated Model object something like:
#Component
public class StringToAttachmentObjectConverter implements Converter<String, Attachment> {
Logger logger = LoggerFactory.getLogger(StringToAttachmentObjectConverter.class);
#Autowired
private ObjectMapper objectMapper;
DocumentContext docContext = null;
#Override
public Attachment convert(String source) {
try {
String sourceString = JsonPath.using(NetsUtilityJSONDatumUtils.jsonPathConfig).parse(source).jsonString();
return objectMapper.readValue(sourceString, Attachment.class);
} catch (JsonParseException e) {
logger.error("Error While converting the String: \n" + source, e);
} catch (JsonMappingException e) {
logger.error("Error While converting the String: \n" + source, e);
} catch (IOException e) {
logger.error("Error While converting the String: \n" + source, e);
}
return null;
}
}
Not sure if there is any better way or if I am defying any best practice(s) but this did the trick for me.

Swagger use a custom swagger.json file aspnet core

Pretty sure I am missing something clearly obvious but not seeing it.
How can I use my updated swagger.json file?
I took my boilerplate swagger/v1/swagger.json code and pasted it into the editor.swagger.io system. I then updated the descriptions etc, added examples to my models and then saved the contents as swagger.json.
Moved the file into the root of my api application, set the file to copy always.
public void ConfigureServices(IServiceCollection services)
{...
services.AddSwaggerGen(c => { c.SwaggerDoc("V1", new Info {Title = "Decrypto", Version = "0.0"}); });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseSwagger();
//--the default works fine
// app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/V1/swagger.json", "Decrypto v1"); });
app.UseSwaggerUI(c => { c.SwaggerEndpoint("swagger.json", "Decrypto v1"); });
app.UseMvc();
}
I have tried a few different variation but none seem to be the trick. I don't really want to rewrite the work in SwaggerDoc as it seems dirty to me put documentation in the runtime.
the custom swagger.json file I want to use looks like this:
{
"swagger": "2.0",
"info": {
"version": "0.0",
"title": "My Title"
},
"paths": {
"/api/Decryption": {
"post": {
"tags": [
"API for taking encrypted values and getting the decrypted values back"
],
"summary": "",
"description": "",
"operationId": "Post",
"consumes": [
"application/json-patch+json",
"application/json",
"text/json",
"application/*+json"
],
"produces": [
"text/plain",
"application/json",
"text/json"
],
"parameters": [
{
"name": "units",
"in": "body",
"required": true,
"schema": {
"uniqueItems": false,
"type": "array",
"items": {
"$ref": "#/definitions/EncryptedUnit"
}
}
}
],
"responses": {
"200": {
"description": "Success",
"schema": {
"uniqueItems": false,
"type": "array",
"items": {
"$ref": "#/definitions/DecryptedUnit"
}
}
}
}
}
}
},
"definitions": {
"EncryptedUnit": {
"type": "object",
"properties": {
"value": {
"type": "string",
"example": "7OjLFw=="
},
"initializeVector": {
"type": "string",
"example": "5YVg="
},
"cipherText": {
"type": "string",
"example": "596F5AA48A882"
}
}
},
"DecryptedUnit": {
"type": "object",
"properties": {
"encrypted": {
"type": "string",
"example": "7OjLV="
},
"decrypted": {
"type": "string",
"example": "555-55-5555"
}
}
}
}
}
you need to configure PhysicalFileProvider and put your swagger.json into wwwroot or anywhere accessible by PhysicalFileProvider. After that you can access it using IFileProvider
Reference: https://www.c-sharpcorner.com/article/file-providers-in-asp-net-core/
Edit If you just add app.UseStaticFiles(); into your StartUp, you can access wwwroot without hastle.
Reference
Completely Different Approach
you may also consider to serve your file using Controller/Action
public IActionResult GetSwaggerDoc()
{
var file = Path.Combine(Directory.GetCurrentDirectory(),
"MyStaticFiles", "swagger.json");
return PhysicalFile(file, "application/json");
}
.NET Core 2.2 could server physical file to url resource like below.
But if you use custom swagger json, your api is fixed except you change it every time.
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
...
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(),
"swagger/v1/swagger.json")),
RequestPath = "swagger/v1/swagger.json"
});
}

swagger: how to validate formData

So far I'm able to do swagger validation if the parameters are from "in": "body" or if the input expected is in a json format.
However, I can't find how to validate a simple string entered as formData.
Below is my swagger script (in json format)
v1swag = {
"cancels_post": {
"tags": ["/api/v1"],
"parameters": [
{
"name": "token",
"in": "formData",
"type": "string",
"required": True,
"description": "Cancels the provided token.",
}
],
"responses": {
"200": {
"description": "Success!",
}
}
}
}
I removed the schema as it seems to only work for "in": "body"
I've been searching the net but can't seem to find the light.
Though I will still be searching... Any hints would be greatly appreciated.
Thank you very much in advance.
A different source media type has to be consumed here. Specify "consumes" member to include media type of application/x-www-form-urlencoded.
v1swag = {
"cancels_post": {
"tags": ["/api/v1"],
"consumes": [
"application/x-www-form-urlencoded"
],
"parameters": [
{
"name": "token",
"in": "formData",
"type": "string",
"required": True,
"description": "Cancels the provided token.",
}
],
"responses": {
"200": {
"description": "Success!",
}
}
}
}

How to parse json with mixed child objects using Sprint Rest Template

I have a json response which looks like below
{
"resourceType": "Topic",
"metadata": {
"lastUpdated": "2016-12-15T14:51:33.490-06:00"
},
"entry": [
{
"resource": {
"resourceType": "Outcome",
"issue": [
{
"response": "error",
"code": "exception"
},
{
"response": "success",
"code": "informational"
},
{
"response": "success",
"code": "informational"
}
]
}
},
{
"resource": {
"resourceType": "Data",
"id": "80",
"subject": {
"reference": "dataFor/80"
},
"created": "2016-06-23T04:29:00",
"status": "current"
}
},
{
"resource": {
"resourceType": "Data",
"id": "90",
"subject": {
"reference": "dataFor/90"
},
"created": "2016-06-23T04:29:00",
"status": "current"
}
}
]
}
Data and Outcome Class extends Resource.
I am using Spring RestTemplate.getForObject(url, someClass). I get below error
has thrown exception, unwinding now
org.apache.cxf.interceptor.Fault: Could not read JSON: Unrecognized field "response" (Class com.model.Resource), not marked as ignorable
at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream#77a3e67a;
I understand that the json is not getting parsed to the child class of Resource. I want to do something like RestTemplate.getForObject(url, someClass) but this is not supported by java generics (wildcard). Please help
You'll want to use jackson to deserialize to a dynamic type, using resourceType as the field to indicate the actual type. Add these to your Resource class.
#JsonTypeInfo(property = "resourceType", use = Id.NAME)
#JsonSubTypes({ #Type(Data.class),
#Type(Outcome.class)
})
Here is a unit test that will prove out the behavior.
#Test
public void deserializeJsonFromResourceIntoData () throws IOException {
Data data = (Data) new ObjectMapper().readValue("{" +
" \"resourceType\": \"Data\"," +
" \"id\": \"80\"," +
" \"subject\": {" +
" \"reference\": \"dataFor/80\"" +
" }," +
" \"created\": \"2016-06-23T04:29:00\"," +
" \"status\": \"current\"" +
" }", Resource.class);
assertEquals(Integer.valueOf(80), data.getId());
assertEquals("dataFor/80", data.getSubject().getReference());
}
As for the cast, I've done it here just to demonstrate that it works, however, to be truly polymorphic, you probably want to have Resource contain all the behavior you need, and then everything is just a Resource.

Location response header returned from web-api to Swagger-ui

I am returning a Uri as the location header of the response from my web-api controller, as shown below:
[HttpPost]
public HttpResponseMessage PostTenant([FromBody] JObject value)
{
string tenantName;
try
{
tenantName = value.GetValue("name").ToString();
}
catch (Exception e)
{
throw new HttpResponseException(
this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, e));
}
try
{
DbInteract.InsertTenant(tenantName.Replace(" ", string.Empty));
var uri = Url.Link("TenantHttpRoute2", new { tenantName });
var response = new HttpResponseMessage(HttpStatusCode.Created);
response.Headers.Location = new Uri(uri);
return response;
}
catch...
}
The swagger-ui client makes the POST request, but the response headers don't contain the Location uri. It appears like this:
POST request from Swagger-ui
As you can see in the image, the RESPONSE HEADERS are
{
"content-type": null
}
The swagger JSON for this post request is:
"post": {
"tags": [
"Tenant"
],
"summary": "Add a new tenant",
"description": "Adds a tenant and returns admin user account information to be used in further calls.",
"consumes": [
"application/json",
"application/xml"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "Tenant",
"description": "Name of the tenant must be alphanumeric without any special characters.",
"required": true,
"schema": {
"$ref": "#/definitions/CreateTenant"
}
}
],
"responses": {
"201": {
"description": "Tenant Inserted",
"headers": [
{
"description": "Location",
"type": "string"
}
]
},
"400": {
"description": "Invalid JSON Format of input"
},
"405": {
"description": "Tenant by this name already exists; Use PUT instead"
},
"500": {
"description": "InternalServerError"
}
}
}
What's wrong here? Why can't I see the response header in swagger-ui? Please let me know if you need more information. Thanks
The intent seems clear enough: a successful response returns a 201 Created with a Location header pointing at the newly created resource. Should work ...
I might remove the "produces" attribute, since you haven't defined any response anywhere.
Try with:
"headers" : { "Location" : { "description": "...", "type" : "string" } }
Instead of:
"headers" : []
so object instead of arrays.

Resources