1

I am developing a Quarkus service-based application for which I am adding open API based annotations such as @ExampleObject. For this, I would like to add the resources file contents as an example that can appear in the SwaggerUI.

I am getting the following error when I add the reference to the files from the resources folder:

Errors
 
Resolver error at paths./api/generateTestData.post.requestBody.content.application/json.examples.Example1 Schema.$ref
Could not resolve reference: Could not resolve pointer: /Example1.json does not exist in document

Resolver error at paths./api/generateTestData.post.requestBody.content.application/json.examples.Example2 Schema.$ref
Could not resolve reference: Could not resolve pointer: /Example2.json does not exist in document

Following is my Quarkus based Java code:

@RequestBody(description = "InputTemplate body",
        content = @Content(schema = @Schema(implementation = InputTemplate.class), examples = {
                @ExampleObject(name = "Example-1",
                        description = "Example-1 for InputTemplate.",
                        ref = "#/resources/Example1.json"), externalValue = "#/resources/Example2.json"
                @ExampleObject(name = "Example-2",
                        description = "Example-2 for InputTemplate.",
                        ref = "#/resources/Example1.json") //externalValue = "#/resources/Example1.json"
        }))

Note: I am able to add the String as value but the content for these examples is very large so I would like to read from the files only so trying this approach.

Is there any way I can access the resources file and add it as a ref within my @ExampleObject

BATMAN_2008
  • 2,788
  • 3
  • 31
  • 98

3 Answers3

2

A working example below:


Create an OASModelFilter class which implements OASFilter:

package org.acme;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.OpenAPI;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

import org.eclipse.microprofile.openapi.models.examples.Example;

public class OASModelFilter implements OASFilter {

    ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void filterOpenAPI(OpenAPI openAPI) {

        //openApi.getComponents() will result in NULL as we don't have any openapi.yaml file.
        Components defaultComponents = OASFactory.createComponents();
        if(openAPI.getComponents() == null){
            openAPI.setComponents(defaultComponents);
        }

        generateExamples().forEach(openAPI.getComponents()::addExample);
    }

    Map<String, Example> generateExamples() {


        Map<String, Example> examples = new LinkedHashMap<>();

        try {

            //loop over your Example JSON Files,..
            //In this case, the example is only for 1 file.
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            InputStream userJsonFileInputStream = loader.getResourceAsStream("user.json");

            String fileJSONContents = new String(userJsonFileInputStream.readAllBytes(), StandardCharsets.UTF_8);



            //Create a unique example for each File/JSON
            Example createExample = OASFactory.createExample()
                                              .description("User JSON Description")
                                              .value(objectMapper.readValue(fileJSONContents, ObjectNode.class));

            // Save your Example with a Unique Map Key.
            examples.put("createExample", createExample);

        } catch (IOException ioException) {
            System.out.println("An error occured" + ioException);
        }
        return examples;
    }

}

The controller using createExample as its @ExampleObject.

@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @APIResponses(
            value = {
                    @APIResponse(responseCode = "200", content =  @Content(
                            mediaType = "*/*",
                            examples = {
                                    @ExampleObject(name = "boo",
                                            summary = "example of boo",
                                            ref = "createExample")
                            }

                    ))
            }
    )
    public String hello() {
        return "Hello RESTEasy";
    }
}

In your application.properties, specify the following: Take note that it references the full package path of the Filter.

mp.openapi.filter=org.acme.OASModelFilter

Contents of user.json file:

{
  "hello": "world",
  "my": "json",
  "testing": "manually adding resource JSONs as examples"
}

The JSON file used is located directly under resources. Of course you can change that path, but you need to update your InputStream.

enter image description here


mvn clean install

mvn quarkus:dev

Go to http://localhost:8080/q/swagger-ui/ and you will now see your user.json file contents displayed

enter image description here

Hopes this helps you,

References for my investigation:

https://github.com/labcabrera/rolemaster-core/blob/c68331c10ef358f6288518350c79d4868ff60d2c/src/main/java/org/labcabrera/rolemaster/core/config/OpenapiExamplesConfig.java

https://github.com/bf2fc6cc711aee1a0c2a/kafka-admin-api/blob/54496dd67edc39a81fa7c6da4c966560060c7e3e/kafka-admin/src/main/java/org/bf2/admin/kafka/admin/handlers/OASModelFilter.java

JCompetence
  • 6,997
  • 3
  • 19
  • 26
  • Thanks a lot for the response. I was able to understand to a certain extent. Can you please tell me how to use the class `OASModelFilter `? I mean where should I call this class `OASModelFilter `? Because I see you have added the `@ExampleObject(ref = "createExample"))` but I do not see the usage of class `OASModelFilter `. It would be really nice if you can provide some more info. Thanks a lot again – BATMAN_2008 Apr 01 '22 at 14:49
  • @BATMAN_2008 I got it working, and updated the answer. – JCompetence Apr 01 '22 at 19:56
  • Wow super. Thanks a lot for this. This is working like a charm :) – BATMAN_2008 Apr 02 '22 at 15:44
  • Can you guys check my answer? It is much easier than described in this answer. – Jimmy Apr 03 '22 at 15:42
  • @SMA Hello, Can you please let me know how can I add the `XML` files to my examples? It does not work the same way as the JSON as we are using the `ObjectMapper and ObjectNode.class`. I tried using the `XMLMapper from Jackson` but still not working. Can you please let me know if there is any way to add `XML` example files similar to `JSON`? – BATMAN_2008 Apr 19 '22 at 08:03
  • 1
    @BATMAN_2008 how about putting "" inside `value(" – JCompetence Apr 19 '22 at 11:05
  • @SMA Thanks a lot for your response. This is working as expected. Thanks a lot again :). Following worked: `examples.put("xmlDocument", OASFactory.createExample().value(xmlFileContent));` – BATMAN_2008 Apr 19 '22 at 11:25
  • 1
    @BATMAN_2008 anytime! – JCompetence Apr 19 '22 at 11:27
  • @SMA Hello, I am stuck at one more point so thought of getting your guidance. I would like to know if there is any way to add the `@ExampleObject` without referencing the `file name ref` so it can take all the file contents from `OASModelFilter examples`. I am using the dynamic approach to load all the files so I am not aware of the file name provided in `Map` but I would like to load all the file contents in `@ExampleObject`. Is there any way to achieve this? Previously we were using `examples.put("createExample"` and using the `name` in `@ExampleObject`. – BATMAN_2008 Apr 20 '22 at 10:12
  • @SMA Now I am looking for some way where I can load all the schema files present within the `examples` rather than providing the `@ExamplePorject` one by one as I am using the dynamic approach and the number of files is pretty huge in number. Can you please let me know if there is any straight forward way to achieve this? – BATMAN_2008 Apr 20 '22 at 10:14
  • @BATMAN_2008 `examples` as part of the `ApiResponse` annotation takes an array of examples. Problem is, it must be constant. So your idea of *generating an array of examples* and plugging into the annotation wont work as is. Not really sure how your idea would be implemented. – JCompetence Apr 20 '22 at 12:26
  • @SMA Thanks a lot for your response. The `Map examples` I am creating in the `OASModelFilter` are actually constant. The only problem is that I am assigning the name of the file to it but when I run the code I do not the name of the files which will happen dynamically so when I add the `@ExampleObject` I would like to load everything present within the `Map examples` rather than we picking manually just like we are doing in `ref = "createExample"`. In the `ref` part I would like to use a logic something like loop over all the elements in `examples` and add. – BATMAN_2008 Apr 20 '22 at 12:58
1

The below works, but as you can see I am creating the PATHS, and you still need to know what the (path/address/is) in order to create paths.

It could help you in thinking in approaching it in a different way.

If you are considering modifying the @ApiResponses/@ApiResponse annotations directly, then it wont work.

package org.acme;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.OpenAPI;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

import org.eclipse.microprofile.openapi.models.examples.Example;

import io.quarkus.logging.Log;

public class CustomOASFilter implements OASFilter {

    ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void filterOpenAPI(OpenAPI openAPI) {

        //openApi.getComponents() will result in NULL as we don't have any openapi.yaml file.
        Components defaultComponents = OASFactory.createComponents();
        if (openAPI.getComponents() == null) {
            openAPI.setComponents(defaultComponents);
        }

        generateExamples().forEach(openAPI.getComponents()::addExample);


        openAPI.setPaths(OASFactory.createPaths()
                                   .addPathItem(
                                           "/hello/customer", OASFactory.createPathItem()
                                                                        .GET(
                                                                                OASFactory.createOperation()
                                                                                          .operationId("hello-customer-get")
                                                                                          .summary("A simple get call")
                                                                                          .description("Getting customer information")
                                                                                          .responses(
                                                                                                  OASFactory.createAPIResponses()
                                                                                                            .addAPIResponse(
                                                                                                                    "200", OASFactory.createAPIResponse()
                                                                                                                                     .content(OASFactory.createContent()
                                                                                                                                                        .addMediaType("application/json", OASFactory.createMediaType()
                                                                                                                                                                                                    .examples(generateExamples()))))))));

    }

    Map<String, Example> generateExamples() {

        Map<String, Example> examples = new LinkedHashMap<>();

        try {

            ClassLoader loader = Thread.currentThread().getContextClassLoader();

            String userJSON = new String(loader.getResourceAsStream("user.json").readAllBytes(), StandardCharsets.UTF_8);
            String customerJson = new String(loader.getResourceAsStream("customer.json").readAllBytes(), StandardCharsets.UTF_8);

            Example userExample = OASFactory.createExample()
                                            .description("User JSON Example Description")
                                            .value(objectMapper.readValue(userJSON, ObjectNode.class));

            Example customerExample = OASFactory.createExample()
                                                .description("Customer JSON Example Description")
                                                .value(objectMapper.readValue(customerJson, ObjectNode.class));

            examples.put("userExample", userExample);
            examples.put("customerExample", customerExample);

        } catch (IOException ioException) {
            Log.error(ioException);
        }
        return examples;
    }

}

enter image description here

JCompetence
  • 6,997
  • 3
  • 19
  • 26
  • Thanks a lot again for your time on this topic. Just to confirm is this an alternative approach for what you have mentioned in the one more answer below where we are creating `public class OASModelFilter implements OASFilter` and setting the package in `application.properties`? – BATMAN_2008 Apr 22 '22 at 08:17
  • No it is the same. The only concern I have for this is that as you can see, I am creating Paths/Path through code, so it wont be as part of the actual Controller code, and not as (infront of the developer eyes) as I would like it to be. It is up to you. Automating things can lead to confusion sometimes, so perhaps comment your Controller so it is understandable where the OpenAPI is getting generated from. – JCompetence Apr 22 '22 at 08:28
  • Thanks a lot for the clarification. Surely, it is nice to have this way as well but TBH I really like the old approach as it seems pretty clean and straight forward :) – BATMAN_2008 Apr 22 '22 at 08:47
  • If you get a chance can you please once have a look at this question which is based on your previous answer and please provide some sort of alternative because I am trying different approach without much success from some time now: https://stackoverflow.com/q/71964917/7584240 – BATMAN_2008 Apr 22 '22 at 08:49
0

EDIT: This is working well in spring-boot

The above answer might work but it has too much code to put into to make it work.

Instead, you can use externalValue field to pass on the JSON file.

For example,

@ExampleObject(
  summary = "temp",
  name =
      "A 500 error",
  externalValue = "/response.json"
)

And now you can create your json file under /resources/static like below,

enter image description here

Swagger doc screenshot

enter image description here

And that's all you need. You don't need to write any manual code here.

Hope this will help you fix the issue.

Jimmy
  • 1,719
  • 3
  • 21
  • 33
  • hello @Jimmy , I am curious does this work for you? Have you actually tested it? The reason I ask, is because: 1) any static files to be served in Quarkus need to be under `src\main\resources\META-INF\resources` 2) There is an open issue https://github.com/swagger-api/swagger-ui/issues/5433 though I cant say it is the reason it is not working for Quarkus. If it does work for you, then kindly please update your answer to show exactly how it is working for you, including the generated Swagger UI. Would be awesome to know if I am missing something – JCompetence Apr 04 '22 at 06:42
  • @Jimmy Thanks a lot for your response. I have tried the `externalValue` before but it did not work for me. Now as you mentioned I moved the files to `resources/static` and tried `externalValue` again and this is not working for me as well. Can you please let me know how its working for you? – BATMAN_2008 Apr 04 '22 at 07:58
  • @SMA Thanks for your comment, I tried this in my `Quarkus-OpenAPI` code, and it's not working for me. – BATMAN_2008 Apr 04 '22 at 07:59
  • hi guys, yes this is working well for me in spring-boot but for Quarkus I think you might need to move this resource to some other location. Maybe what @SMA has suggested maybe? Because spring-boot read its all resources under /static folder, hence I added under static. – Jimmy Apr 04 '22 at 08:04
  • Even with the Quarkus as well I believe we will be able to achieve this. – Jimmy Apr 04 '22 at 08:07
  • It does not work with Quarkus, even when the json file is under `src\main\resources\META-INF\resources` I have tested it. – JCompetence Apr 04 '22 at 08:11
  • @SMA What error are you getting when you try to open json in swagger doc? – Jimmy Apr 04 '22 at 08:13
  • No error at all @Jimmy It will simply display the swagger documentation, but nothing under the example JSON part. That part will be empty. – JCompetence Apr 04 '22 at 08:19
  • @SMA try to see if you can access that JSON file on your local URL something like http://localhost:8080/response.json if you can access that file then swagger will be able to read as well. – Jimmy Apr 04 '22 at 08:22
  • PS: I have added a screenshot to the answer and also added the note that it is working for spring-boot. – Jimmy Apr 04 '22 at 08:33
  • @Jimmy Working for Springboot is irrelevant to Quarkus. Yes, the URL does work if I go to` http://localhost:8080/user.json` if I put the user.json file under `src\main\resources\META-INF\resources` – JCompetence Apr 04 '22 at 09:08
  • 1
    This is not working for Spring Boot, try this solution and failed – Randy Hector May 02 '22 at 17:05
  • @RandyHector make sure your app has no issues with CORS. – Jimmy May 06 '22 at 12:10
  • Yes, I checked the CORS and they are not a problem. Do you have a link to share with a minimum example @Jimmy – Randy Hector May 18 '22 at 13:34