[swagger configuration input parameters ignore certain fields]

Article directory

    • 1. Background
        • 1 Introduction
        • 2. Existing practice 1
        • 3. Method 2 supported by springfox
    • 2. Question
    • Three, ideas
    • Fourth, realize
        • 1. Define the annotation SwaggerInputFieldIgnore
        • 2. Implement WebMvcOpenApiTransformationFilter
        • 3. Implement WApiListingBuilderPlugin
    • Five, results

One, background

1. Introduction

Use springfox to generate the swagger.json file, and then import it into yapi for display; when the request and response used have a common model, there will generally be fewer model fields that need to be input, and more model information fields will be output.

for example:
Taking user management as an example, the following structure is defined

/**
 * swagger user testing method
 *
 * @author ruoyi
 */
@Anonymous
@Api("User Information Management")
@RestController
@RequestMapping("/test/user")
public class TestController extends BaseController
{
    private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
    {
        users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
        users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
    }

    @ApiOperation("Get user list")
    @GetMapping("/list")
    public R<List<UserEntity>> userList()
    {
        List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
        return R.ok(userList);
    }

    @ApiOperation("Add user")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "userId", value = "userid", dataType = "Integer", dataTypeClass = Integer.class),
        @ApiImplicitParam(name = "username", value = "username", dataType = "String", dataTypeClass = String.class),
        @ApiImplicitParam(name = "password", value = "User password", dataType = "String", dataTypeClass = String.class)
// @ApiImplicitParam(name = "mobile", value = "User's mobile phone", dataType = "String", dataTypeClass = String.class)
    })
    @PostMapping("/save")
    public R<String> save(@ApiIgnore UserEntity user)
    {
        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
        {
            return R.fail("User ID cannot be empty");
        }
        users.put(user.getUserId(), user);
        return R.ok();
    }

    @ApiOperation("Update user")
    @PutMapping("/update")
    public R<String> update(@RequestBody UserEntity user)
    {
        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
        {
            return R.fail("User ID cannot be empty");
        }
        if (users.isEmpty() || !users.containsKey(user.getUserId()))
        {
            return R.fail("User does not exist");
        }
        users.remove(user.getUserId());
        users.put(user.getUserId(), user);
        return R.ok();
    }

}

@Data
@ApiModel(value = "UserEntity", description = "UserEntity")
class UserEntity extends BaseEntity
{
    @ApiModelProperty("User ID")
    private Integer userId;

    @ApiModelProperty("user name")
    private String username;

    @ApiModelProperty("User password")
    private String password;

    @ApiModelProperty("user mobile phone")
    private String mobile;

}

In this way, the UserEntity model is shared during user update and user list query. The fields common to most entities (BaseEntity), such as creation time, update time, whether to delete, etc. are also displayed in the request API on the Yapi side, but for front-end development students, these are not necessary.

Our goal is: when passing parameters on the front end, only the necessary parameters are used, and other parameters are not displayed.

2. Existing practice 1

Define a new DTO, such as UpdateUserDTO, which only contains some necessary fields

Disadvantages:
A new class is added, which requires copying some fields each time, which is somewhat redundant.

3. Method supported by springfox 2

Use the @ApiIgnore annotation as done when inserting the user in the above action.

Disadvantages:
It is necessary to use @ApiImplicitParams and @ApiImplicitParam annotations, which is troublesome to write when there are many parameters.

Second, question

All of the above are quite cumbersome. There will be conversion coding operations after adding the model DTO; when using @ApiIgnore and @ApiImplicitParam, you need to write a lot of annotation configurations that have nothing to do with business logic; there is some workload when updating both; so is there a solution? A better way, which can not only share the model entity (UserEntity), but also use a small amount of coding to remove various non-essential fields during user input

Three, ideas

step1. Add the annotation @SwaggerInputFieldIgnore to identify the fields that are ignored when used as input parameters.
step2. When swagger loads various @API configurations, parse out which model entities are used, especially the models identified by @SwaggerInputFieldIgnore.
step3. When generating swagger.json content, process the field identified by @SwaggerInputFieldIgnore through reflection and ignore the field in the result.

Four, implementation

The idea is relatively clear. During the actual coding implementation process, it is necessary to have the necessary knowledge reserves for the automatic configuration principle of swagger and the mechanism of spring-plugin. I organized the final coding results as follows:

1. Define the annotation SwaggerInputFieldIgnore
/**
 * In the swagger schema model,
 * 1. In the scenario where it is used as a body parameter, certain fields are ignored
 * 2. In the scenario where it is used as an output parameter, there is no need to ignore certain fields.
 *
 * for example:
 * @ApiOperation("Get user list, UserEntity as parameter")
 * @GetMapping("/list")
 * public R<List<UserEntity>> userList();
 *
 * @ApiOperation("Update user, UserEntity as input parameter")
 * @PutMapping("/update")
 * public R<String> update(@RequestBody UserEntity user)
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SwaggerInputFieldIgnore {
}
2. Implement WebMvcOpenApiTransformationFilter
@Component
public class InputFieldIgnoreOpenApiTransformationFilter implements WebMvcOpenApiTransformationFilter {
    private static final Logger log = getLogger(InputFieldIgnoreOpenApiTransformationFilter.class);
    public static final String SUFFIX_INPUT = "-input";

    private Map<String, InjectedInputSchema> injectedSchemaMap = new HashedMap();
    @Autowired
    private ModelRetriveApiListingPugin modelRetriveApiListingPugin;

    @Value("${swagger.fieldIgnore.Enable:true}")
    private Boolean fieldIgnoreEnable;

    public static final class InjectedInputSchema {
        String originKey;
        String injectedKey;
        Class schemaClazz;
        Schema injectedSchema;

        public InjectedInputSchema(String originKey, Class schemaClazz) {
            this.originKey = originKey;
            this.injectedKey = originKey + "-input";
            this.schemaClazz = schemaClazz;
        }
    }

    /**
     * 1. Parse the remaining fields according to the annotation @SwaggerInputFieldIgnore
     * 2. Add corresponding schema
     *
     * @param oas
     */
    private void processInputInject(OpenAPI oas) {
        if (!fieldIgnoreEnable) {
            return;
        }

        // 1. Parse paths and replace $ref; at the same time, collect schema metadata to supplement schema-input.
        for (PathItem each : oas.getPaths().values()) {
            appendRefWithInput(each.getPut());
            appendRefWithInput(each.getPost());
        }

        if(MapUtils.isEmpty(injectedSchemaMap)){
            return;
        }

        // 2. Supplement schema-input
        for (InjectedInputSchema each : injectedSchemaMap.values()) {
            // 2.1. Construct schema
            Schema old = oas.getComponents().getSchemas().get(each.originKey);
            Schema schema = constructSchema(each, old);
            // 2.2. Add oas.components.schemas
            oas.getComponents().getSchemas().put(each.injectedKey, schema);
        }
    }

    private void appendRefWithInput(Operation operation) {
        if (operation == null || operation.getRequestBody() == null) {
            return;
        }
        try {
            Content content = operation.getRequestBody().getContent();
            MediaType mediaType = content.get(org.springframework.http.MediaType.APPLICATION_JSON_VALUE);
            Schema schema = mediaType.getSchema();
            String $ref = schema.get$ref();
            String originName = $ref.substring($ref.lastIndexOf("/") + 1);
            QualifiedModelName modelName = modelRetriveApiListingPugin.getApiModels().get(originName);
            if (modelName != null) {
                schema.set$ref($ref + SUFFIX_INPUT);
                injectedSchemaMap.put(originName, new InjectedInputSchema(originName, constructClazz(modelName)));
            }
        } catch (Exception e) {
            log.error("error occured", e);
        }
    }

    private static Class<?> constructClazz(QualifiedModelName modelName) throws ClassNotFoundException {
        return Class.forName(modelName.getNamespace() + "." + modelName.getName());
    }

    private Schema constructSchema(InjectedInputSchema each, Schema old) {

        Schema result = new ObjectSchema();
        result.title(each.injectedKey);
        result.type(old.getType());
        result.description(old.getDescription());

        HashMap<String, Schema> props = new HashMap<>(old.getProperties());
        Set<String> removingKey = new HashSet();
        props.keySet().forEach(filedName -> {
            Field field = ReflectionUtils.findField(each.schemaClazz, filedName);
            SwaggerInputFieldIgnore anno = AnnotationUtils.findAnnotation(field, SwaggerInputFieldIgnore.class);
            if (anno != null) {
                removingKey.add(filedName);
            }
        });

        removingKey.forEach(field -> props.remove(field));
        result.setProperties(props);

        return result;
    }

    @Override
    public OpenAPI transform(OpenApiTransformationContext<HttpServletRequest> context) {
        OpenAPI openApi = context.getSpecification();
        processInputInject(openApi);
        return openApi;
    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return delimiter == DocumentationType.OAS_30;
    }
}

3. Implement WApiListingBuilderPlugin
@Order(value = Ordered.LOWEST_PRECEDENCE)
@Component
public class ModelRetriveApiListingPugin implements ApiListingBuilderPlugin {
    private static final Logger log = LoggerFactory.getLogger(ModelRetriveApiListingPugin.class);

    private Map<String, QualifiedModelName> apiModels = new HashedMap();

    @Override
    public void apply(ApiListingContext apiListingContext) {
        Field filed = ReflectionUtils.findField(ApiListingBuilder.class, "modelSpecifications");
        filed.setAccessible(true);
        Map<String, ModelSpecification> specsMap = (Map<String, ModelSpecification>) ReflectionUtils.getField(filed, apiListingContext.apiListingBuilder());

        retriveApiModels(specsMap.values());
    }

    private void retriveApiModels(Collection<ModelSpecification> specs) {
// Collection<ModelSpecification> specs = each.getModelSpecifications().values();
        specs.forEach(spec -> {
            ModelKey modelKey = spec.getCompound().get().getModelKey();
            QualifiedModelName modelName = modelKey.getQualifiedModelName();
            apiModels.put(modelName.getName(), modelName);
            log.info(modelName.toString());
        });
    }

    Map<String, QualifiedModelName> getApiModels() {
        return apiModels;
    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return true;
    }
}

Five, results

  1. After adding the above java file and setting the model field annotations, you can see that the output swagger.json content has ignored the annotation fields in the input parameters.
  2. After importing yapi, the fields ignored by the configuration are no longer available, _!