You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用com.epages.restdocs-api-spec 0.18.2记录multipart/form-data请求

问题描述

我在开发Spring Boot项目时,使用com.epages.restdocs-api-spec 0.18.2版本生成API文档,但在记录接收multipart/form-data的端点时遇到问题——生成的OpenAPI 3 YAML文件完全缺失请求部分的定义,仅保留了响应内容。

以下是我的代码和配置细节:

控制器代码

@PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<EraseAreaRequestDto.Response> home(
        @ModelAttribute @Validated RequestDto dto
) {
    log.info("original filename: {}", dto.getOriginalImageFile().getOriginalFilename());
    log.info("masking filename: {}", dto.getMaskingImageFile().getOriginalFilename());
    log.info("original url: {}", dto.getOriginalImageUrl());
    log.info("masking url: {}", dto.getMaskingImageUrl());
    log.info("test1: {}", dto.getRequest3().getTest1());
    log.info("test2: {}", dto.getRequest3().getTest2());

    var response = new EraseAreaRequestDto.Response();
    response.setOriginalFilename(dto.getOriginalImageFile().getOriginalFilename());
    response.setMaskingFilename(dto.getMaskingImageFile().getOriginalFilename());
    response.setOriginalImageUrl(dto.getOriginalImageUrl());
    response.setMaskingImageUrl(dto.getMaskingImageUrl());
    return ResponseEntity.ok(response);
}

RequestDto 定义

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class RequestDto {

    private MultipartFile originalImageFile;

    private MultipartFile maskingImageFile;

    private String originalImageUrl;

    private String maskingImageUrl;
}

测试代码

@Test
public void documentMultipart() throws Exception {
            Part part = new MockPart("originalImageFile", "originalImageFile", new byte[10], MediaType.IMAGE_JPEG);
            Part part2 = new MockPart("maskingImageFile", "maskingImageFile", new byte[10], MediaType.IMAGE_JPEG);
            Part originalImageUrlPart = new MockPart("originalImageUrl", null, "originalImageUrl".getBytes(StandardCharsets.UTF_8), MediaType.TEXT_PLAIN);
            Part maskingImageUrlPart = new MockPart("maskingImageUrl", null, "maskingImageUrl".getBytes(StandardCharsets.UTF_8), MediaType.TEXT_PLAIN);

            return mockMvc.perform(RestDocumentationRequestBuilders.multipart(API_ERASE_AREA)
                    .part(part, part2, originalImageUrlPart, maskingImageUrlPart, part3)
                    .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)
                    .characterEncoding(StandardCharsets.UTF_8))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.originalFilename").exists())
                .andExpect(jsonPath("$.maskingFilename").exists())
                .andExpect(jsonPath("$.originalImageUrl").exists())
                .andExpect(jsonPath("$.maskingImageUrl").exists())
                .andDo(document(
                        "{Identifier}",
                        resource(
                                ResourceSnippetParameters.builder()
                                        .summary("Summary")
                                        .description("description")
                                        .requestSchema(schema("RequestDto"))
                                        .formParameters(
                                                parameterWithName("originalImageUrl").description("adsf").optional().type(SimpleType.STRING),
                                                parameterWithName("maskingImageUrl").description("asdf").optional(),
                                                parameterWithName("originalImageFile").description("asdf").optional(),
                                                parameterWithName("maskingImageFile").description("asdf").optional()
                                        )
                                        .responseSchema(schema("EraseAreaRequestDto.Response"))
// skip...                                        .responseFields() 
                                        .tags("tag")
                                        .build())
                ));
}

Gradle 配置

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
    id 'org.graalvm.buildtools.native' version '0.10.2'
    id 'com.epages.restdocs-api-spec' version "0.18.2"
}

ext {
    springShellVersion = "3.3.0"
    host = 'localhost:8888'
    apiUrl = '/'
}

group = 'com.asd'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.shell:spring-shell-starter'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // restdoc
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    testImplementation "com.epages:restdocs-api-spec-mockmvc:0.18.2"
    testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2'
    testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.shell:spring-shell-dependencies:$springShellVersion"
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

task copyYaml {
    logger.info("copyYaml -> ${project.buildDir}/resources/main/public/docs")

    copy {
        from "build/api-spec"
        into "${project.buildDir}/resources/main/static/docs"
    }
}

openapi3 {
    delete "build/api-spec"
    delete "build/generated-snippets"
    server = apiUrl
    title = 'test'
    description = "test test"
    tagDescriptionsPropertiesFile = 'src/docs/tag-descriptions.yaml'
    version = '0.1.0'
    format = 'yaml'
}

openapi {
    delete "build/api-spec"
    delete "build/generated-snippets"
    host = 'localhost:8888'
    title = 'test'
    description = "test test"
    tagDescriptionsPropertiesFile = 'src/docs/tag-descriptions.yaml'
    version = '0.1.0'
    format = 'yaml'
}

当前生成的 OpenAPI 3 YAML

openapi: 3.0.1
info:
  title: test
  description: test test
  version: 0.1.0
servers:
- url: /
tags: []
paths:
  /api/sample:
    post:
      tags:
      - "tag"      
      summary: summary
      description: description
      operationId: {idenifier}
      responses:
        "200":
          description: "200"          
          content:
            application/json;charset=UTF-8:
              schema:
                $ref: '#/components/schemas/RequestDto.Response'              
              examples:
                identifier:
                  value: "{\"originalFilename\":\"originalImageFile\",\"maskingFilename\":\"maskingImageFile\",\"originalImageUrl\":\"originalImageUrl\",\"maskingImageUrl\":\"maskingImageUrl\",\"list\":null,\"array\":null}"

我需要让com.epages.restdocs-api-spec正确生成multipart/form-data类型的请求定义,确保所有字段(文件和普通字符串)都能被完整记录到OpenAPI 3文档中;如果OpenAPI 3无法实现,OpenAPI 2也可以接受,但必须保证请求的所有字段都能被正确描述。


解决方案

针对com.epages.restdocs-api-spec 0.18.2版本记录multipart/form-data请求的问题,可通过以下步骤修复:

1. 调整测试代码的文档配置

requestSchema不适用于multipart/form-data类型请求,需改用requestParts分别定义每个表单部分,并指定对应媒体类型:

修改测试中的resource配置:

.resource(
    ResourceSnippetParameters.builder()
        .summary("Summary")
        .description("description")
        // 移除requestSchema,改用requestParts定义每个multipart部分
        .requestParts(
            partWithName("originalImageFile")
                .description("原始图片文件")
                .optional()
                .contentType(MediaType.IMAGE_JPEG_VALUE),
            partWithName("maskingImageFile")
                .description("遮罩图片文件")
                .optional()
                .contentType(MediaType.IMAGE_JPEG_VALUE),
            partWithName("originalImageUrl")
                .description("原始图片URL")
                .optional()
                .contentType(MediaType.TEXT_PLAIN_VALUE)
                .type(SimpleType.STRING),
            partWithName("maskingImageUrl")
                .description("遮罩图片URL")
                .optional()
                .contentType(MediaType.TEXT_PLAIN_VALUE)
                .type(SimpleType.STRING)
        )
        .responseSchema(schema("EraseAreaRequestDto.Response"))
        .tags("tag")
        .build()
)

2. 给RequestDto添加Schema注解

为了让OpenAPI正确识别字段类型,给RequestDto添加@Schema注解明确字段信息:

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "Multipart请求参数")
public class RequestDto {

    @Schema(description = "原始图片文件", type = "string", format = "binary")
    private MultipartFile originalImageFile;

    @Schema(description = "遮罩图片文件", type = "string", format = "binary")
    private MultipartFile maskingImageFile;

    @Schema(description = "原始图片URL")
    private String originalImageUrl;

    @Schema(description = "遮罩图片URL")
    private String maskingImageUrl;
}

3. 验证生成结果

修改后重新运行测试,生成的OpenAPI 3 YAML会包含完整的请求定义,示例如下:

paths:
  /api/sample:
    post:
      tags:
      - "tag"      
      summary: summary
      description: description
      operationId: {identifier}
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                originalImageFile:
                  type: string
                  format: binary
                  description: 原始图片文件
                maskingImageFile:
                  type: string
                  format: binary
                  description: 遮罩图片文件
                originalImageUrl:
                  type: string
                  description: 原始图片URL
                maskingImageUrl:
                  type: string
                  description: 遮罩图片URL
            required: []
      responses:
        "200":
          description: "200"          
          content:
            application/json;charset=UTF-8:
              schema:
                $ref: '#/components/schemas/RequestDto.Response'              
              examples:
                identifier:
                  value: "{\"originalFilename\":\"originalImageFile\",\"maskingFilename\":\"maskingImageFile\",\"originalImageUrl\":\"originalImageUrl\",\"maskingImageUrl\":\"maskingImageUrl\",\"list\":null,\"array\":null}"

注意事项

  • com.epages.restdocs-api-spec 0.18.2版本对multipart/form-data的支持依赖requestParts,formParameters或requestSchema主要用于application/x-www-form-urlencoded类型的表单请求。
  • 若需兼容OpenAPI 2,只需将Gradle中的openapi3配置改为openapi即可,上述requestParts配置同样适用。

内容的提问来源于stack exchange,提问作者김도연

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 11:17:02