Swagger declaration schema = @Schema(implementation = Map.class) represents Schema as String in swagger-ui

31.8k views Asked by At

I am trying to create springdoc swagger documentation, and I would like to represent a request body having data type Map<String, Object> in a better readable way for clients. But when I declare @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(schema = @Schema(implementation = Map.class) the Schema is coming as String(attached the screenshot below)

enter image description here

Method declaration

        @Operation(security = {@SecurityRequirement(name = "bearer-key")}, summary = "Create Data", operationId = "createData", description = "Create createData for the **`type`**. ")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "201", description = "Data created", content = @Content(schema = @Schema(implementation = Map.class),
                    examples = {@ExampleObject(value = "{\n" +
                            "    \"id\": \"927d810c-3ac5-4584-ba58-7c11befabf54\",\n" +
                            "}")})),
            @ApiResponse(responseCode = "400", description = "BAD Request")})
    @PostMapping(value = "/data/type", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
    @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(schema = @Schema(implementation = Map.class),
            examples = {@ExampleObject(value = "{\n" +
                    "            \"label\":\"tourism\",\n" +
                    "            \"location\":\"France\"\n" +
                    "         }")}))
    ResponseEntity<Map<String, Object>> createData(@Parameter(name = "type", required = true) @PathVariable("type") String type, @Parameter(name = "request payload") @Valid @RequestBody Map<String, Object> body);

Though the Spring boot automatically infers the type based on the method signature, it is not clear for the data type Map. For instance, by default, the type Map<String, Object> will be inferred as below enter image description here

But I would like to show the Schema in a more understandable way for the client who refers to my API. I could see there is a closed ticket without a proper solution in Github. As per my requirement, the request body should be a type agnostic and dynamic key-value pairs, so there is no other way apart from receiving the request as Map<String, Object>. has anyone implemented a better way with type Map rather than creating a custom request/response model?

6

There are 6 answers

1
Prasanth Rajendran On BEST ANSWER

Sharing my working approach for the issue, I have done a workaround for the @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(schema = @Schema(implementation = Map.class) the Schema is coming as String issue.

I have declared a custom schema called Map in the OpenAPI bean declaration as below

new OpenAPI()
                .components(new Components()
                        .addSchemas("Map", new Schema<Map<String, Object>>().addProperties("< * >", new ObjectSchema())
                        ))
                    .....
                    .....

and used the above schema in the Schema declaration as below

 @io.swagger.v3.oas.annotations.parameters.RequestBody(
            content = @Content(mediaType = APPLICATION_JSON_VALUE, 
                 schema = @Schema(ref = "#/components/schemas/Map"))

The above approach can be used in the place of ApiResponse also as below

 @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200",
            content = @Content(mediaType = APPLICATION_JSON_VALUE, 
                 schema = @Schema(ref = "#/components/schemas/Map"))

Note: If we use the above custom schema approach, we don't need to alter or ignore any of the types which SpringDoc is using internally.

0
brianbro On

This is the default behaviour of the springdoc-openapi library in order to ignore other injectable parameters supported by Spring MVC.

If you want to change this behaviour, you can just exlcude it as follow:

    SpringDocUtils.getConfig().removeRequestWrapperToIgnore(Map.class);
0
SimpleJack On

I ran into this problem myself today. As it turns out, this is actually a design problem with Swagger (@see related question).

Nonetheless, I tried my hand at it too, using the approaches from here and the other thread.

Here is my OpenAPI with one custom schema for a Map<Integer,String>:

@Configuration
@OpenAPIDefinition(
        info = @io.swagger.v3.oas.annotations.info.Info(
                title = "ACME Inc. REST API",
                version = "1.0",
                description = "This is an overview of all REST endpoints of this application",
                contact = @io.swagger.v3.oas.annotations.info.Contact(name = "John Doe", url = "https://acme-inc.com/", email = "[email protected]")
        )
)
public class OpenAPIConfig {
        public static final String ERROR_CODE_MAPPER = "ErrorCode-Mapper";

        @Bean
        public OpenApiCustomiser openApiCustomiser() {
                return openApi -> {
                        Components components = openApi.getComponents();
                        for(Schema<?> schema: buildCustomSchemas()) {
                                components.addSchemas(schema.getName(), schema);
                        }
                };
        }

        private static List<Schema<?>> buildCustomSchemas() {
                ArrayList<Schema<?>> result = new ArrayList<>();

                Schema<?> integerStringMap = new Schema<Map<Integer, String>>()
                        .name(ERROR_CODE_MAPPER)
                        .type("object")
                        .addProperty("error code", new StringSchema().example("Error message belonging to the error code")).example(getErrorCodeExample());
                result.add(integerStringMap);
                // Build more custom schemas...

                return result;
        }

        private static Map<Integer, String> getErrorCodeExample() {
                Map<Integer, String> example = new HashMap<>();
                example.put(666, "Oh no..., the devil himself  showed up and stopped your request");
                return example;
        }
}

(NOTE: Look up your swagger source code io.swagger.v3.oas.models.media for useful utility classes like StringSchema. You don't have write everything from scratch.)

And this is my REST endpoint:

@Operation(summary = "This endpoint returns a list of system error codes, that can occur during processing requests.")
@ApiResponses(value = {
        @ApiResponse(
                responseCode = "200",
                description = "Map of all system error codes mapping to their messages",
                content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(ref = "#/components/schemas/"+ ERROR_CODE_MAPPER))}
        )
})
@GetMapping("/error-codes")
public Map<Integer, String> listErrorCodes() {
    // return your map here...
}

This produces something like this:

enter image description here

It is important to know that in a JSON object the key is always of type string. So the type does not have to be written explicitly. With that in mind, this is the schema:

enter image description here

1
rodiri On

I have one API endpoint, the request body expects a HashMap. There is not much information on how to fix the "Example value" issue. Prasanth's answer lead me to the right place. I'm posting my solution for completeness but all credit goes to him. (PS: I tried to upvote his answer but I don't have enough "points")

The configurations side:

@Configuration
@OpenAPIDefinition
public class DocsConfiguration {
    @Bean
    public OpenAPI customOpenAPI() {
        Schema newUserSchema = new Schema<Map<String, Object>>()
                .addProperties("name",new StringSchema().example("John123"))
                .addProperties("password",new StringSchema().example("P4SSW0RD"))
                .addProperties("image",new StringSchema().example("https://robohash.org/John123.png"));

        return new OpenAPI()
                //.servers(servers)
                .info(new Info()
                        .title("Your app title")
                        .description("App description")
                        .version("1.0")
                        .license(new License().name("GNU/GPL").url("https://www.gnu.org/licenses/gpl-3.0.html"))
                )
                .components(new Components()
                        .addSchemas("NewUserBody" , newUserSchema)
                );
    }
}

The controller side:

    @io.swagger.v3.oas.annotations.parameters.RequestBody(
            content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
                    schema = @Schema(ref = "#/components/schemas/NewUserBody")))
    @PostMapping("/v1/users")
    public Response<User> upsertUser(@RequestBody HashMap<String,Object> user) {
         //Your code here
    }
0
phirzel On
  1. I created a HashMap extension class:

     @Schema(description = "Response-Object Map<String, EcoBalance).")
     public class EcoMap extends HashMap<String, EcoBalance> {
       @JsonIgnore
       @Override
       public boolean isEmpty() {
         return super.isEmpty();
       }
     }
    
  2. use it as response object

     @ApiResponse(responseCode = "200", content = @Content(mediaType = .., schema = @Schema(implementation = EcoMap.class)), headers = ..
    
  3. be aware the OpenAPI 3 generator does not generate such a client-model, but is properly referenced in openapi.yml (and even validates).

0
antonkronaj On

Id like to update rodiri's answer for my situation. I had to combine the answer by rodiri and this answer by Ondřej Černobila to the SO question SpringDoc - How to Add schemas programmatically. I am using java 11, spring-boot-starter-parent 2.5.6, and springdoc-openapi-ui 1.5.12 which I believe is using swagger 3.52.5

<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.5.12</version>
</dependency>

My config

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@OpenAPIDefinition
public class DocsConfiguration {

  @Bean
  public OpenApiCustomiser openApiCustomiser() {
    return openApi -> {
      var NewUserBodySchema = new ObjectSchema()
          .name("NewUserBody")
          .title("NewUserBody")
          .description("Object description")
          .addProperties("name", new StringSchema().example("John123"))
          .addProperties("password", new StringSchema().example("P4SSW0RD"))
          .addProperties("image", new StringSchema().example("https://robohash.org/John123.png"));

      var schemas = openApi.getComponents().getSchemas();
      schemas.put(NewUserBodySchema.getName(), NewUserBodySchema);
    };
  }

}

For my endpoint I am using a get that returns a Map so its different from the accepted answer.

@GetMapping(value = "/{userId}")
@Operation(
    summary = "Get Something",
    description = "Some desciption",
    responses = {
        @ApiResponse(
            responseCode = "200",
            description = "The Map Response",
            content = {
                @Content(
                    mediaType = MediaType.APPLICATION_JSON_VALUE,
                    schema = @Schema(ref = "#/components/schemas/NewUserBody")
                )
            })
    }
)
public ResponseEntity<Map<String, Object>> getMap(@PathVariable String userId)