Skip to content

Latest commit

 

History

351 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spring REST Docs OpenAPI 3 Specification

English | 한국어

jitpack-badge build-badge codecov-badge sonarcloud-badge license-badge

A modified version of the ePages-de/restdocs-api-spec with class field type and constraint inference. And only support OpenAPI 3.0.1 specs.

Build configuration

Versions

Artifact line Spring Boot Spring REST Docs Java bytecode Tested JDKs Status
2.x (main) 4.1.x 4.0.x 17 17, 21, 25 Active (latest release: 2.1.4)
1.x (v1.x) 3.5.x 3.0.x 17 17, 21, 25 (historical) Frozen, unsupported
0.x (v0.x) 2.7.x 2.0.x Frozen, unsupported

The public packages remain under com.keecon.restdocs.*, and the Gradle plugin ID remains com.keecon.restdocs-openapi3 across release lines.

The 2.x line requires Java 17 or newer. CI verifies the LTS JDK releases 17, 21, and 25. Use 1.1.2 for Spring Boot 3.5 applications. Version 2.1.4 is available from JitPack; no Plugin Portal publication is assumed.

See MAINTENANCE.md for the branch lifecycle, backport, and release policy.

Local builds require JDK 25; Java and Kotlin artifacts retain Java 17 compatibility. CI and release verification continue on JDK 17, 21, and 25. Use ./gradlew clean build. The v1.x line is frozen: no further fixes, dependency updates, or automated releases.

Gradle

  1. Add the plugin

    buildscript {
      repositories {
        // ...
        maven { url = uri('https://jitpack.io') }
      }
      dependencies {
        // ...
        classpath 'com.github.keecon.restdocs-openapi3:restdocs-api-spec-gradle-plugin:2.1.4'
      }
    }
    
    apply plugin: 'com.keecon.restdocs-openapi3'
  2. Add required dependencies to your tests

    repositories {
      // ...
      maven { url 'https://jitpack.io' }
    }
    
    dependencies {
      //..
      testImplementation 'com.github.keecon.restdocs-openapi3:restdocs-api-spec:2.1.4'
      testImplementation 'com.github.keecon.restdocs-openapi3:restdocs-api-spec-mockmvc:2.1.4'
    }
    
    openapi3 {
      server = 'http://localhost:8080'
      title = 'My API'
      description = 'My API description'
      tagDescriptionsPropertiesFile = 'src/test/resources/openapi-tags.yml'
      version = '0.1.0'
      format = 'yaml'
    }

2.1.1 DSL additions

On the current main branch, the openapi3 task resolves its default input and output directories from the project's Gradle layout.buildDirectory. With the standard build directory, it reads snippets from build/generated-snippets and writes build/api-spec/openapi3.json or build/api-spec/openapi3.yaml, depending on format.

This build-directory-aware behavior and the additive method and Gradle Action syntax below are available from 2.1.1. With 2.1.0, use the assignment syntax shown above.

Groovy builds can use method syntax:

openapi3 {
  server 'https://api.example.com'
  contact {
    name = 'API Support'
    email = 'support@example.com'
  }
}

After adding the same JitPack buildscript classpath and applying the plugin, Kotlin builds can configure the named extension explicitly:

import com.keecon.restdocs.apispec.gradle.OpenApi3Extension

extensions.configure<OpenApi3Extension>("openapi3") {
    server("https://api.example.com")
    contact {
        name = "API Support"
        email = "support@example.com"
    }
    oauth2SecuritySchemeDefinition {
        flows = arrayOf("authorizationCode")
        tokenUrl = "https://example.com/token"
        authorizationUrl = "https://example.com/authorize"
    }
}

When documented JWT scopes create an OAuth2 security requirement, configure at least one OAuth2 flow. Specification generation fails if the matching OAuth2 security scheme definition is missing.

Migrating from 1.x to 2.x

The 2.x line moves to Spring Boot 4, Spring REST Docs 4, and Jackson 3. Extension-level Groovy configuration remains compatible. Code that configures the openapi3 task directly must adapt to its managed Gradle properties: scalar task getters now return Property<T> and directory getters return DirectoryProperty, so set values with .set(...) instead of assigning the 1.x String/Boolean task properties directly.

2.1.0 features

OpenAPI contact metadata can be configured in the Gradle DSL.

openapi3 {
  contact = {
    name = 'API Support'
    email = 'support@example.com'
    url = 'https://example.com/support'
  }
}

Java time fields are inferred as follows:

Java type OpenAPI type OpenAPI format Typical Jackson text output
LocalDate string date YYYY-MM-DD
OffsetDateTime string date-time RFC 3339 with a numeric offset or Z
Instant string date-time RFC 3339 in UTC with Z
ZonedDateTime string date-time RFC 3339 with a numeric offset; the region ID must be omitted

JSON examples for these inferred Java time fields are validated when the OpenAPI document is generated. The accepted range is an RFC 3339 profile without positive leap seconds: seconds and an offset are required; seconds must range from 00 through 59; T and Z may be uppercase or lowercase; fractions may contain any positive number of digits; and numeric offsets range through +23:59/-23:59, including the unknown-local-offset form -00:00. Uppercase T and Z are recommended for interoperability. All three types accept either Z or a numeric offset; the table shows the typical textual output produced by Jackson.

To keep examples consistent with typical Jackson serialization, leap seconds (:60) are rejected regardless of their historical occurrence. This library does not maintain external leap-second announcements or history.

accepted: 2026-08-30T15:30:00+09:00
accepted: 2026-08-30t06:30:00.123456789012z
rejected: 2026-08-30T15:30:00
rejected: 2026-08-30T15:30:00+09:00[Asia/Seoul]
rejected: 1788071400
rejected: 1990-12-31T23:59:60Z

This library does not configure the application's JSON serializer. Configure Jackson to emit textual date values rather than timestamps, and do not enable zone-ID output for ZonedDateTime. Generation fails if captured JSON request or response body examples violate this contract. Fields declared manually as DATETIME retain their existing validation behavior and are not subject to this inferred-type RFC 3339 validation.

LocalDateTime is intentionally not inferred as date-time: even its textual Jackson representation has no offset, while RFC 3339 date-time requires one. Use Instant, OffsetDateTime, or ZonedDateTime when the value represents an instant.

The WebTestClient integration is available from the restdocs-api-spec-webtestclient module.

dependencies {
  testImplementation 'com.github.keecon.restdocs-openapi3:restdocs-api-spec-webtestclient:2.1.4'
}

Replace Spring REST Docs' WebTestClient document consumer with the wrapper. It keeps the normal REST Docs snippets and adds resource.json from their descriptors.

webTestClient.get()
  .uri('/v1/products/{id}', 1)
  .exchange()
  .expectStatus().isOk()
  .expectBody()
  .consumeWith(WebTestClientRestDocumentationWrapper.document(
    'products-id-get',
    responseFields(fieldWithPath('id').description('product id'))
  ))

Usage with Spring REST Docs

when:
def resultActions = mockMvc.perform(
  post('/v1/products/{productId}/result', 1)
    .contentType(MediaType.APPLICATION_JSON)
    .content(objectMapper.writeValueAsBytes(new ProductResultCreateRequestBody(result)))
    .accept(MediaType.APPLICATION_JSON)
)

then:
def reqModel = Constraints.model(ProductResultCreateRequest.class)
def reqBodyModel = Constraints.model(ProductResultCreateRequestBody.class)
def respModel = Constraints.model(ProductResultCreateResponse.class)
resultActions
  .andExpect(status().isOk())
  .andDo(document('products-id-result-post',
    resource(ResourceSnippetParameters.builder()
      .tag('product')
      .summary('Create a product result')
      .description('''
        |Create a product result
        |
        |### Error details
        |
        |`400` BAD_REQUEST
        |- bad request description
        |
        |`401` UNAUTHORIZED
        |- unauthorized description
        |
        |'''.stripMargin())
      .requestSchema(schema('ProductResultCreateRequest'))
      .pathParameters(
        reqModel.withName('productId').description('product id'),
      )
      .requestFields(
        reqBodyModel.withPath('result').description('product result'),
        reqBodyModel.withPath('result.code').description('product result code'),
        reqBodyModel.withPath('result.seq').description('product result seq'),
        reqBodyModel.withPath('result.score').description('product result score'),
        reqBodyModel.withPath('result.assigns[]').description('result assign object list'),
        reqBodyModel.withPath('result.assigns[].code').description('result assign code'),
        reqBodyModel.withPath('result.assigns[].seq').description('result assign seq'),
        reqBodyModel.withPath('result.assigns[].objectId').description('result assign object id'),
        reqBodyModel.withPath('result.assigns[].fileType').description('result assign file type')
          .optional(),
        reqBodyModel.withPath('result.assigns[].fileUrl').description('result assign file url')
          .optional(),
        reqBodyModel.withPath('result.assigns[].comments[]').description('result assign comment list')
          .type(DataType.ARRAY)
          .attributes(Attributes.items(DataType.STRING, null, null))
          .optional(),
      )
      .responseSchema(schema('ProductResultCreateResponse'))
      .responseFields(
        respModel.withPath('status').description('operation status'),
        respModel.withPath('code').description('product result code')
          .optional(),
      )
      .build())))
when:
def resultActions = mockMvc.perform(
  get('/v1/products/{productId}/result?code={code}', 1, 1)
    .accept(MediaType.APPLICATION_JSON)
)

then:
def reqModel = Constraints.model(ProductResultRequest.class)
def respModel = Constraints.model(ProductResultResponse.class)
resultActions
  .andExpect(status().isOk())
  .andDo(document('products-id-result-code-get',
    resource(ResourceSnippetParameters.builder()
      .tag('product')
      .summary('Get a product result info')
      .description('''
        |Get a product result info
        |
        |### Error details
        |
        |`400` BAD_REQUEST
        |- bad request description
        |
        |`401` UNAUTHORIZED
        |- unauthorized description
        |
        |`404` NOT_FOUND
        |- not found description
        |
        |'''.stripMargin())
      .requestSchema(schema('ProductResultRequest'))
      .pathParameters(
        reqModel.withName('productId').description('product id'),
      )
      .queryParameters(
        reqModel.withName('code').description('product result code'),
        reqModel.withName('seq').description('product result seq')
          .defaultValue(ProductResultRequest.DEFAULT_RESULT_SEQ)
          .optional(),
      )
      .responseSchema(schema('ProductResultResponse'))
      .responseFields(
        respModel.withPath('result').description('product result'),
        respModel.withPath('result.code').description('product result code'),
        respModel.withPath('result.seq').description('product result seq'),
        respModel.withPath('result.score').description('product result score'),
        respModel.withPath('result.assigns[]').description('result assign object list'),
        respModel.withPath('result.assigns[].code').description('result assign code'),
        respModel.withPath('result.assigns[].seq').description('result assign seq'),
        respModel.withPath('result.assigns[].objectId').description('result assign object id'),
        respModel.withPath('result.assigns[].fileType').description('result assign file type')
          .optional(),
        respModel.withPath('result.assigns[].fileUrl').description('result assign file url')
          .optional(),
        respModel.withPath('result.assigns[].comments[]').description('result assign comment list')
          .type(DataType.ARRAY)
          .attributes(Attributes.items(DataType.STRING, null, null))
          .optional(),
      )
      .build())))

About

Clone of ePages-de/restdocs-api-spec with class field type and constraint inference.

Topics

Resources

Stars

2 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages