Spring REST Docs + Asciidoctor: Automated API Documentation
Ask any backend developer what they hate most about maintaining a REST API, and somewhere near the top of the list you’ll hear “the docs are lying again.” Someone changed a field name three sprints ago, nobody updated the Confluence page, and now a partner integration is throwing 400 errors because their client is sending exactly what the documentation told them to send. Sound familiar? This is the single most common failure mode in API documentation, and it’s not a tooling problem so much as a trust problem. Documentation that can drift from reality will, eventually, drift from reality.
Spring REST Docs takes a genuinely different stance on this, and once you’ve worked with it, going back to hand-maintained docs feels a bit like going back to manually formatting a spreadsheet after using a proper database.
What is Spring REST Docs and how does it prevent documentation drift?
Spring REST Docs is a documentation tool that generates API documentation snippets directly from your integration tests, combined with Asciidoctor to produce the final published output. Because the snippets are only written when the underlying test passes, documentation can never describe behavior your API doesn’t actually exhibit. If the test breaks, the docs build breaks with it.
That single mechanism, tying documentation generation to test success, is the entire value proposition in one sentence. Everything else, the Asciidoctor formatting, the request and response templates, the curl examples, is scaffolding around that core guarantee.
Why hand-written docs rot and tests don’t
Here’s a question worth sitting with: why do teams that religiously maintain test coverage somehow let their documentation go stale within a quarter? It’s not laziness, not really. It’s incentive structure. A failing test blocks a merge. A stale doc page blocks nothing. Nobody’s CI pipeline turns red because a Markdown file mentions a field that got renamed last month, so the doc just sits there, quietly wrong, until a confused developer or an angry partner finds it the hard way.
Spring REST Docs closes that gap by making the documentation itself a byproduct of a passing test. You’re not writing documentation and hoping it stays accurate. You’re writing a MockMvc or RestAssured test that actually exercises your endpoint, and the framework captures the real request and real response as it happened, then formats them into Asciidoctor snippets you can drop straight into your docs. There’s genuinely no way for the documented request shape to diverge from what your controller actually accepts, because they’re generated by the same execution.
@Test
void createOrder() throws Exception {
this.mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\": 42, \"quantity\": 3}"))
.andExpect(status().isCreated())
.andDo(document("create-order",
requestFields(
fieldWithPath("productId").description("The id of the product being ordered"),
fieldWithPath("quantity").description("Number of units requested")),
responseFields(
fieldWithPath("id").description("The generated order id"),
fieldWithPath("status").description("Initial status, always PENDING"))));
}
Run this test, and Spring REST Docs writes out a handful of .adoc snippet files, the request body, the response body, a curl command, an HTTP example, all captured from the actual execution. Change your controller so that quantity becomes qty without updating this test, and the test fails outright, because the JSON no longer matches what the assertion expects. There’s no scenario where the snippet quietly documents a field that doesn’t exist anymore. The official Spring REST Docs reference walks through the full range of snippet types this mechanism supports, and it’s worth spending an afternoon with if you’ve never set this up before.

The diagram above traces this exact pipeline, a test calling the endpoint, snippets being written only on success, those snippets getting pulled into a hand-written Asciidoc source file, and the whole thing compiling into published HTML. It’s a short chain, but every link in it is load-bearing.
How Asciidoctor pulls it all together
Snippets alone aren’t documentation, they’re ingredients. Asciidoctor is what turns them into something a developer would actually want to read. You write a .adoc file with your prose, your context, your explanation of why an endpoint behaves the way it does, and you pull in the generated snippets using include directives.
== Creating an order
A `POST` request creates a new order for the authenticated customer.
===== Request fields
include::{snippets}/create-order/request-fields.adoc[]
===== Example request
include::{snippets}/create-order/curl-request.adoc[]
===== Example response
include::{snippets}/create-order/http-response.adoc[]
What’s nice about this split is that it respects what each side is actually good at. Humans write context, rationale, edge cases, gotchas, the stuff a machine can’t infer. Tests generate the mechanical, verifiable parts, exact field names, exact status codes, exact payload shapes. Asciidoctor’s table support, cross-references, and conditional includes give you enough structural power to build something that reads like real documentation rather than an auto-dumped API reference, and its attribute and include-file handling makes it straightforward to keep large multi-endpoint docs organized without one enormous unreadable file.
A small tip that saves real pain later: use Asciidoctor tables for anything involving repeated structured data, like a list of error codes and their meanings, rather than trying to force that into prose. It reads better and it’s far easier to scan.
Spring REST Docs versus the annotation-driven alternative
If you’ve worked with Swagger or OpenAPI-generated documentation before, this whole approach probably feels almost inverted, and it kind of is. The annotation-driven model, where you decorate your controller methods with @Operation, @ApiResponse, and friends, generates documentation from declarations in your code. That’s fast to set up and gives you an interactive try-it-out console for free, genuinely valuable for external developers exploring an API for the first time. But the annotations are just metadata. Nothing forces them to stay synchronized with what the endpoint actually does. Change a validation rule, forget to update the annotation, and your beautiful interactive docs are now confidently wrong.
Spring REST Docs trades that convenience for verification. You lose the try-it-out console unless you build one separately, and setting it up takes real effort compared to sprinkling annotations across a controller. What you gain is a guarantee that annotations simply can’t offer: what’s documented is what was tested, full stop.

The comparison diagram above lays this trade-off out directly, source of truth on one side versus the other, and the very different failure modes each approach produces when things drift.
Plenty of teams, honestly, run both. Swagger or OpenAPI for the public-facing interactive exploration experience, Spring REST Docs as the internal source of truth that actually gets verified in CI. It’s not either-or if your documentation needs justify the extra build complexity.
Getting it running in a real project
Setting this up in a Gradle project is a handful of blocks, not a rewrite of your build. You add the org.asciidoctor.jvm.convert plugin, the org.springframework.restdocs dependency, point your test source set at a snippets output directory, and wire the asciidoctor task to depend on your test task so documentation can never build from stale or absent snippets.
plugins {
id 'org.asciidoctor.jvm.convert' version '4.0.3'
}
ext { snippetsDir = file('build/generated-snippets') }
test { outputs.dir snippetsDir }
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
That dependsOn test line is doing more work than it looks like. It means running the documentation build without running the tests first simply isn’t possible, which closes off the one remaining way someone could accidentally publish docs built from an outdated snippet directory.
Frequently Asked Questions (FAQ)
Does Spring REST Docs work with WebTestClient for reactive applications, or only MockMvc?
It works with both, along with REST Assured for full integration-style tests against a running server. The spring-restdocs-webtestclient module gives you the same snippet-generation mechanics for WebFlux-based reactive endpoints, so reactive teams don’t lose this guarantee just because they’re not using the traditional Servlet stack.
Is Spring REST Docs a replacement for OpenAPI, or can they coexist in the same project?
They coexist well, and plenty of teams run both intentionally. Some even generate an OpenAPI specification from Spring REST Docs snippets using community tooling, getting the interactive console experience on top of the verified, test-backed accuracy Spring REST Docs guarantees underneath.
How much extra effort does Spring REST Docs actually add to a typical sprint?
For a team already writing solid integration tests, the incremental effort is genuinely small, mostly adding document() calls and field descriptions to tests you were writing anyway. The upfront cost is real when you’re establishing the Asciidoctor structure and build wiring for the first time, but that’s a one-time investment, not a recurring tax per endpoint.
What happens to the generated documentation if a field is optional versus required?
Spring REST Docs won’t infer this automatically from your Java model, you need to declare it explicitly using .optional() on a field descriptor, and the framework will still fail the test if a field you marked as required is actually missing from the captured response, keeping that distinction verified rather than assumed.




