14

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?

2
  • Saw the issue here? Commented Nov 25, 2020 at 18:20
  • yeah all injectable parameter types supported by Spring MVC are excluded and ignored by the swagger, the complete list is available in the link, the suggested solution is not most elegant and may not suit for all requirement mainly if your reponse type is Map<String, Object> then suggested approach will not suit Commented Nov 25, 2020 at 18:47

8 Answers 8

15

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.

Sign up to request clarification or add additional context in comments.

1 Comment

thanks! To generate an ApiClient by openapi-generator-maven-plugin I had to set skipValidateSpec=true because of the schema Map at install
10

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
    }

1 Comment

Thanks for your compliment, your solution perfectly fits if there are one or two APIs with a Map input body, but if there are 10 to 15 API that expects the inputs, it will be a cumbersome process to construct the 15 schemas and configure.
2

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) 

Comments

1

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);

Comments

1

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

Comments

1

Today, I've faced the same issue. It's fixed in new Swagger versions. Nowadays, it's possible to declare dictionaries, hashmaps and associative arrays. OpenAPI lets you define dictionaries where the keys are strings. The example below produces the same result as the accepted question:

import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.StringToClassMapItem;

@ApiResponse(
        responseCode = "200",
        content = @Content(
        mediaType = "application/json",
        schema = @Schema(
                type = "object",
                properties = {@StringToClassMapItem(key = "<*>", value = Object.class)}
        )
))
Map<String, Object> createData(..) {...}

To define a dictionary, use type = object within content schema. Then, define fixed key/values within properties as in the example above.

Comments

0
  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).

Comments

0

I have encountered the same issue, and frankly - none of the solutions were good enough for me. My workaround was to force the swagger to guess the retuning type

@ApiResponses(
@ApiResponse(
    responseCode = "200",
    **useReturnTypeSchema = true**
)
ResponseEntity<Map<String, String>> xxx

)

This made the swagger to document the endpoint as returning additionalProperties schema, which I was expecting

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.