How to document JSON Merge Patch endpoints in OpenAPI? - swagger-ui

I have implemented a REST API which is documented via OpenAPI. In detail, the specification is generated from Java source code using springdoc-openapi.
However, I have the need for accepting patches. So, I
/customers/{id}:
patch:
tags:
- Customers
summary: Updates an existing user
description: Updates an existing user
operationId: partialUpdateMergePatchCustomer
parameters:
- name: id
in: path
description: The numeric ID of the customer
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/merge-patch+json:
schema:
type: object
required: true
I have read through the OpenAPI specification/documentation but did not find any information regarding JSON Merge Patch, JSON Patch or the like.
I am experiencing the following issues I'd like to overcome:
There is no clue information of which type of resource is to be patched here. The rest of the API, esp. in SwaggerUI, is well documented and has schema references everywhere. How can I specify the resources schema that actually is to be patched?
Apart from the pure schema reference, in my API, some resources have read-only fields or there may be other restrictions that apply to certain fields that I'd like to document. Is there a better way as doing it in natural language as part of the original schema?

I had the same issue as your issue #1. That is, providing more info to swagger on the JsonPatch RequestBody parameter than the default generated by springdoc-openapi. I'm posting my solution here for others who may run into the same issue.
My solution was to add the following annotation to the RestController patch method:
#PatchMapping(value = "/{id}", consumes = "application/json-patch+json")
#io.swagger.v3.oas.annotations.parameters.RequestBody(content = #Content(array = #ArraySchema(schema = #Schema(implementation = JsonPatchSchema.class))))
public void patch(#PathVariable #Min(0) long id, #RequestBody JsonPatch patch) {
Where JsonPatchSchema contains the requested format:
public class JsonPatchSchema {
#NotBlank
public Op op;
public enum Op {
replace, add, remove, copy, move, test
}
#NotBlank
#Schema(example = "/name")
public String path;
#NotBlank
public String value;
}
Using the #Schema annotation you could provide more detail on what is allowed, see the Schema javadoc. For example the pattern parameter allows you to define a regular expression. I haven't found a way to generate the allowed content unfortunately.

Related

OpenAPI, contract-first with Springdoc and multiple specs

I am trying to port a Swagger UI from Springfox to Springdoc.
The Swagger UI is generated via Maven plugin
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
... from two different OpenAPI specifications, both implemented in the same Spring Boot application. We use two different specs for two different clients.
So far, my Swagger UI looked like this with Springfox. Note the drop-down list that allows to choose the spec to display.
Now, with Springdoc 1.6.6, the Swagger-UI looks like this.
Note that:
The default landing page is the sample Pet Store and none of my specifications, although I specified in my Spring Boot's application.yaml:
springdoc:
swagger-ui:
disable-swagger-default-url: true
No drop-down
I have to manually enter my specification's name into the "Explore" text field in order to see its Swagger UI
I tried addressing the missing drop-down by following I have installed OpenAPI 3 using springdoc, but the URL is strange. Can I change it to the expected value?, which claims to display a drop-down, but to no avail.
My questions:
How do I display the drop-down? Does Springdoc support it at all out-of-the-box?
Apparently, accessing http://localhost:8080/swagger-ui/index.html?urls.primaryName=Information makes the drop-down list of specs magically appear.
How do I make sure that the default URL http://localhost:8080/swagger-ui/index.html lands on one of my specs, and not on the Pet Store?
Here is btw. my Bean configuration:
#Configuration
public class SwaggerDocumentationConfig {
/**
* Path of the OpenAPI package extracted out of a random class in that package
*/
private static final String INFORMATION_PACKAGE = InformationApi.class.getPackageName();
/**
* Path of the OpenAPI package extracted out of a random class in that package
*/
private static final String OPERATIONS_PACKAGE = OperationsApi.class.getPackageName();
private GroupedOpenApi getBaseApiDoc(String groupName, String packagePath) {
return GroupedOpenApi.builder()
.group(groupName)
.packagesToScan(packagePath)
.build();
}
#Bean
public GroupedOpenApi getOperationsApiDoc() {
return getBaseApiDoc("Operations", OPERATIONS_PACKAGE);
}
#Bean
public GroupedOpenApi getInfoApiDoc() {
return getBaseApiDoc("Information", INFORMATION_PACKAGE);
}
#Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("SpringShop API")
.description("Spring shop sample application")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("SpringShop Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}}

Generate OpenAPI descriptions from JavaDoc

I have an application which provides an API with JAX-RS (Java API for RESTful Web Services / JSR-311).
For documentation purposes I provide an URL according to the OpenAPI-Specification, which is generated by Eclipse MicroProfile OpenAPI.
Everything is working fine, except the descriptions of the methods and parameters, which I need to add twice - in annotations and in JavaDoc:
/**
* Finds all resources with the given prefix.
*
* #param prefix
* the prefix of the resource
* #return the resources that start with the prefix
*/
#GET
#Path("/find/{prefix}")
#Produces(MediaType.APPLICATION_JSON)
#Operation(description = "Finds all resources with the given prefix")
public List<Resource> find(
#Parameter(description = "The prefix of the resource")
#PathParam("prefix") final String prefix) {
...
}
I know that no runtime library can read the JavaDoc (because it is not part of the class files), which is the main reason for the annotations. But I wonder if there is some other option for one of the OpenAPI generation tools (Swagger, Eclipse MicroProfile OpenAPI, ...), which prevents me from manually syncing the documentation?
In another project for example I'm using a doclet which serializes the JavaDoc and stores it in the class path, to present an Beans API documentation to the user at runtime. But even if I make use of this doclet here, I see no option to provide that JavaDoc descriptions to the OpenAPI libraries during runtime.
I know that I could drop the JavaDoc, if the users of my API use only "foreign languages", as they wouldn't see the JavaDoc anyway. But what happens if the other side of the API is a JAX-RS client? In that case the JavaDoc would be a huge support.
I got it running with Eclipse Microprofile OpenAPI.
I had to define my own OASFilter:
public class JavadocOASDescriptionFilter implements OASFilter {
#Override
public void filterOpenAPI(final OpenAPI openAPI) {
openAPI.getComponents().getSchemas().forEach(this::initializeSchema);
openAPI.getPaths().forEach(this::initializePathItem);
}
private void initializeSchema(final String name, final Schema schema) {
final SerializedJavadoc javadoc = findJavadocForSchema(name);
if (StringUtils.isEmpty(schema.getDescription())) {
schema.setDescription(javadoc.getTypeComment());
}
if (schema.getProperties() != null) {
schema.getProperties().forEach((property, propertySchema) -> {
if (StringUtils.isEmpty(propertySchema.getDescription())) {
propertySchema.setDescription(javadoc.getAttributeComments().get(property));
}
});
}
}
...
}
Then I had to declare that filter in META-INF/microprofile-config.properties:
mp.openapi.filter=mypackage.JavadocOASDescriptionReader
See here for the discussion on this topic: https://github.com/eclipse/microprofile-open-api/issues/485

Smallrye open api interceptor

I am developing a rest application.
Some endpoints require a custom header parameter, not related to authorisation. I created a custom annotation using jax-rs NameBinding. Here is an usage example:
#GET
#RequiresBankHeader
public int get(
#HeaderParam("bank")
#Parameter(ref = "#/components/parameters/banks")
String bank) {
return someService.getSomeInformation();
}
There is a provider that intercepts this call and do some routine using the information in the header parameter.
The problem is that I have to repeat '#HeaderParam("bank") #Parameter(ref = "#/components/parameters/banks") String bank' everywhere, just so it appears in Swagger, even though the service classes do not need it. I was able to at least reuse the parameter definition with ref = "#/components/parameters/banks", and declaring it in the OpenAPI.yml file, that Quarkus merges with generated code very nicely.
But I also want to create and interceptor to dynamically add this do the OpenApi definition whenever RequiresBankHeader annotation is present.
Is there a way to do it?
I dont think you can use interceptors to modify the generated Openapi schema output.
If all methods on a given endpoint require some parameter, you can specify it on class level like so:
#Path("/someendpoint")
public class MyEndpoint {
#HeaderParam("bank")
#Parameter(name = "bank")
String bank;
#GET
public Response getAll() {return Response.ok().build()}
#GET
#Path("{id}")
public Response someMethod(#PathParam("id") String id) {return Response.ok().build();}
}
As mentioned by Roberto Cortez, the MP OpenAPI spec provides a programmatic way to contribute metadata to the openapi.yml file.
It is not possible to detect an annotation in the JAX-RS endpoint definition, but it was good enough to automate what I needed. Since all methods that had the RequiresBankHeader return the same Schema, I was able to hack it like this:
public class OpenApiConfigurator implements OASFilter {
#Override
public Operation filterOperation(Operation operation) {
operation.getResponses().getAPIResponses().values().stream().
map(APIResponse::getContent).
filter(Objects::nonNull).
map(Content::getMediaTypes).
flatMap(mediaTypes -> mediaTypes.values().stream()).
map(MediaType::getSchema).
filter(Objects::nonNull).
map(Schema::getRef).
filter(Objects::nonNull).
filter(ref -> ref.contains("the common response schema")).
findAny().
ifPresent(schema -> {
ParameterImpl parameter = new ParameterImpl();
parameter.setRef("#/components/parameters/banks");
operation.addParameter(parameter);
});
return operation;
}
OpenApiConfigurator should be configure in the application properties, using mp.openapi.filter=com.yourcompany.OpenApiConfigurator

Request body not showing in Nest.js + Swagger

My controller code is something like this.
#Controller('customer')
export class CustomerController{
constructor(private readonly customerService: CustomerService){}
#Post('lookup')
async someMethod(#Body() body:any){
console.log("BEGIN -- CustomerController.someMethod");
I am expecting to see in Swagger a place where I can input some text as a request body but instead I see this
Add #ApiProperty()
export class User{
#ApiProperty()
name:string
}
So it sounds like there's a few things going on here. The Swagger UI is a helper tool for sending in requests, but to do that it needs to know the shape of the request body. any is not good enough. If you're looking for a tool that allows you to send in anything, curl or postman are you best bets (at least for free).
Nest has a Swagger plugin that will read through your Typescript code and decorate your types and method accordingly, but you have to opt in to enable it. Otherwise, you need to use the decorators from the #nestjs/swagger package to tell Swagger what types are expected in and out of the methods.
So long as the type that corresponds to #Body() has swagger decorators or you enable the swagger plugin and have a valid class, the swagger UI should show up as expected, but with the above and using type any it won't do you any good.
My endpoint accepts unknown key/value data and I was having the same problem (I tried any, unknown, Record<string, any>, object, {}). Finally #Body() data: Map<string, any> worked for me.
try it like this:
#ApiBody({description: "body:any someMethod"})
#Post('lookup')
async someMethod(#Body() body:any){
console.log("BEGIN -- CustomerController.someMethod");
}
I would recommend using dto for body.
Refer to documentation.
Example of a DTO is shown below.
DTO:
import { ApiProperty } from '#nestjs/swagger';
export class CreateCatDto {
#ApiProperty()
name: string;
#ApiProperty()
age: number;
#ApiProperty()
breed: string;
}
Function
#Post()
async create(#Body() createCatDto: CreateCatDto) {
//Do Stuff.
}
#ApiProperty adds properties to swagger request.
It should show something like this:
#Post()
#ApiBody({ type: CreateCatDto })
async create(#Body() createCatDto: CreateCatDto) {
//Do Stuff.
}
The above code is going to give output similar to below where your schema is also going to be documented:
Hope this helps.
Swagger is unable to interpret your code. It is not an issue with your code. If your goal is to test your API for the UI team to use and not a comprehensive swagger documentation, then I found it easiest to just use Postman. Try hitting your API using Postman.

Cannot serialize to Delta<> in an OData Controller

I'm implementing an ODataController. It's OData V3 for compatibility reasons with Breeze.js:
using System.Web.Http.OData;
public class OffersController : ODataMetadataController
{
...
Somwhere in the middle I want to implement merge/patch as seen in examples:
[AcceptVerbs("PATCH", "MERGE")]
public IHttpActionResult Patch([FromODataUri] int key, Delta<BOOffer> delta)
{
...
For some reason I'm getting the following error:
No MediaTypeFormatter is available to read an object of type 'Delta`1' from content with media type 'application/json'.;
Ok. Delta<> is OData related, I would need an OData formatter for that.
Iterating through the formatters (as on this page), it does not seem to be an OData formatter there:
JsonMediaTypeFormatter
CanReadType: True
CanWriteType: True
Base: BaseJsonMediaTypeFormatter
Media Types: application/json, text/json
XmlMediaTypeFormatter
CanReadType: True
CanWriteType: True
Base: MediaTypeFormatter
Media Types: application/xml, text/xml
FormUrlEncodedMediaTypeFormatter
CanReadType: False
CanWriteType: False
Base: MediaTypeFormatter
Media Types: application/x-www-form-urlencoded
JQueryMvcFormUrlEncodedFormatter
CanReadType: True
CanWriteType: False
Base: FormUrlEncodedMediaTypeFormatter
Media Types: application/x-www-form-urlencoded
Should I register this formatter? Shouldn't it be automatic? If I need to register it manually, how?
If I change the input parameter form Delta<BOOffer> to BOOffer the method gets called, but since only the changed properties are sent, this is not something I can use.
I configure my controller in app_start like this:
System.Web.Http.OData.Builder.ODataConventionModelBuilder builderV3 = new System.Web.Http.OData.Builder.ODataConventionModelBuilder();
var entitySetConfigV3 = builderV3.EntitySet<BOOffer>("Offers");
entitySetConfigV3.EntityType.HasKey(o => o.ID);
config.Routes.MapODataServiceRoute(
routeName: "odata/v3",
routePrefix: "odata/v3",
model: builderV3.GetEdmModel(),
batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
The reason for this was referencing both System.Web.Http.OData (odatav3) and System.Web.OData (odatav4) in the project and mixing up references.
the System.Web.Http.OData.Formatter.ODataMediaTypeFormatter is not configured to be able to serialize into System.Web.OData.Delta<T>.
Using to System.Web.Http.OData.Delta<T> worked as intended.
Be careful about referencing different OData versions in one projects.

Resources