Let's get started with a Microservice Architecture with Spring Cloud:
Documenting Form Parameters With Spring REST Docs
Last updated: October 6, 2026
1. Introduction
Form-encoded endpoints are common in login and subscription flows, but easy to misunderstand without precise request documentation. API consumers need to know which parameters are required, which are optional, which values are valid, and which extra parameters are intentionally ignored.
In this tutorial, we’ll document form parameters for a Spring MVC endpoint by using Spring REST Docs and MockMvc tests.
It’s important to note that although related, query parameters serve different use cases and are documented with different REST Docs snippets, so we won’t cover them here.
2. Why Document Form Parameters?
In form submissions, everything starts as key-value pairs. Without documentation, there is room for ambiguity:
- Are all fields required?
- Which values do constrained fields accept?
- Are there ignorable parameters?
Spring MVC handles all this for us, but API consumers still need exact details. Spring REST Docs generates documentation snippets after a the test being documenteded passes.
It’s important to note that REST Docs doesn’t convert arbitrary assertions into documentation automatically. We still describe each parameter programmatically. The benefit we get is that we get the documentation only from the tests that pass and validate the behavior of the code.
Note that we’ll handle only form parameters in this article, not the query parameters:
- Form parameters belong to a request body encoded as application/x-www-form-urlencoded
- Query parameters are part of the URL, for example /newsletter/subscriptions?frequency=weekly. They serve different use cases and are documented with different REST Docs snippets.
3. Building a Form-Based Endpoint
Let’s build a small newsletter subscription API that receives form fields, validates the payload, and returns a JSON response.
Basically, our form model represents request fields from application/x-www-form-urlencoded data. Spring binds parameters to this object by matching parameter names to JavaBean property names.
So, let’s start with our subscription form. We use Bean Validation annotations to enforce required and constrained values:
public class SubscriptionForm {
@Email
@NotBlank
private String email;
@NotBlank
private String name;
@NotBlank
@Pattern(regexp = "weekly|monthly")
private String frequency;
private List<String> topics;
private boolean marketingAccepted;
// standard getters and setters
}
The pattern for frequency explicitly states valid values. If a client sends a value that isn’t weekly or monthly, validation will fail before business logic runs. Note that Spring REST Docs doesn’t infer that constraint, so it doesn’t automatically add valid values to the resulting documentation.
4. Test Setup and Request Construction
The main idea of REST Docs is generating snippets from tested behavior. So, we use MockMvc with REST Docs configuration to create a request that includes all supported form fields.
4.1. Test Setup and Snippet Naming
Our test class uses @WebMvcTest, and we also need to specify the RestDocumentationExtension so JUnit can manage the RestDocumentationContext from Spring REST Docs:
@WebMvcTest
@ExtendWith(RestDocumentationExtension.class)
class NewsletterControllerDocumentationTest {
@Autowired
private MockMvc mockMvc;
@BeforeEach
void setUp(
WebApplicationContext context, RestDocumentationContextProvider restDocumentation) {
this.mockMvc = webAppContextSetup(context)
.apply(documentationConfiguration(restDocumentation))
.alwaysDo(document("{method-name}"))
.build();
}
}
Our setUp() method prepares the context for each test:
- apply(documentationConfiguration(restDocumentation)) attaches Spring REST Docs to the MockMvc instance built by webAppContextSetup(), so it captures requests and responses.
- alwaysDo(document(“{method-name}”)) adds a default documentation generation action that runs for every mockMvc.perform() in our test. The {method-name} placeholder captures the current test method name.
When we want a fixed snippet directory name, we set an explicit identifier in the test itself, like document(“newsletter-subscribe”). That creates snippets under target/generated-snippets/newsletter-subscribe.
4.2. The Form Request
Let’s break down each part of the test, starting with a simple mock payload that includes the form fields. We use the param() method to send key-value pairs to an endpoint that accepts form payloads:
private MockHttpServletRequestBuilder postSubscription() {
return post("/newsletter/subscriptions")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("email", "[email protected]")
.param("name", "Baeldung API")
.param("frequency", "weekly")
.param("topics", "spring", "testing")
.param("marketingAccepted", "true");
}
Note that we send topics twice with param(“topics”, “spring”, “testing”), which is how it handles multi-value form fields.
5. Documenting Form Parameters
Now, we’re ready for documentation.
5.1. Describing Form Parameters
We use formParameters() to document the form payload. We define a name and a description, using optional() for non-required parameters:
private FormParametersSnippet docFormParams() {
return formParameters(
parameterWithName("email").description("The subscriber email address"),
parameterWithName("name").description("The display name of the subscriber"),
parameterWithName("frequency").description("Delivery frequency: weekly or monthly"),
parameterWithName("topics").optional()
.description("One or more selected topic values"),
parameterWithName("marketingAccepted").optional()
.description("Whether marketing messages are accepted"));
}
If valid frequency values change, we need to remember to manually update the parameter description, as the @Pattern annotation doesn’t infer it.
Most importantly, optional() controls how Spring REST Docs documents a parameter. It doesn’t affect Bean Validation. Constraints such as @NotBlank apply independently when Spring validates the form object.
If we add or remove a required parameter, one of two things usually happens: either request validation behavior changes and the test fails, or descriptor matching fails because our documented parameters no longer match the request.
5.2. Dealing With Bean Validation Constraints
To avoid describing constraints manually, we can read validation metadata through ConstraintDescriptions and incorporate it into parameter descriptions by calling descriptionsForProperty().
Let’s create a helper method to get the constraint description for the SubscriptionForm class and frequency property:
private String constraintsFor(String property) {
ConstraintDescriptions constraints = new ConstraintDescriptions(SubscriptionForm.class);
List<String> frequencyConstraints = constraints.descriptionsForProperty("frequency");
}
Now, we can go back to docFormParams() and use our constraintsFor() method:
parameterWithName("frequency")
.description("Delivery frequency. Constraints: " + constraintsFor("frequency")),
This is the description we get:
Delivery frequency. Constraints: Must match the regular expression `weekly|monthly`, Must not be blank
So, it includes descriptions for all validation annotations. If they change, the description changes, too.
5.3. Defining Ignored Parameters
Some clients send extra form values that we don’t want to include in our API documentation. For example, frontend tracking parameters are irrelevant to business logic. By default, REST Docs fails with a SnippetException if it finds any undocumented parameter, so we have to ignore them explicitly.
Let’s start by going back to postSubscription() and sending the trackingId parameter that we’ll ignore later:
private MockHttpServletRequestBuilder postSubscription() {
return post("/newsletter/subscriptions")
// previous parameters ...
.param("trackingId", "campaign-42");
}
Now, in docFormParams(), we add trackingId as an ignored parameter:
private FormParametersSnippet docFormParams() {
return formParameters(
// earlier parameters ...
parameterWithName("trackingId").ignored());
}
This way, the parameter can appear, but it isn’t part of the API docs we get in the end.
6. Executing the Test and Generating Snippets
Finally, let’s put all those parts together in a mock request:
@Test
void whenFormRequestIsValid_thenDocumentFormParameters() throws Exception {
mockMvc.perform(postSubscription())
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").isNumber())
.andDo(
document("newsletter-subscribe",
docFormParams()));
}
This single test validates the endpoint and defines field-level documentation for all form parameters. Most importantly, it helps keep the contract in one place.
When we run our documented test, Spring REST Docs writes snippet files under target/generated-snippets/newsletter-subscribe, which was the name we chose with the document() method.
6.1. Generated Form Snippet
The generated snippet for our form POST test execution shows the parameters in a structured format, using the descriptions we provided with the parameterWithName() calls:
|===
|Parameter|Description
|`+email+`
|The subscriber email address
|`+name+`
|The display name of the subscriber
|`+frequency+`
|Delivery frequency: weekly or monthly
|`+topics+`
|One or more selected topic values
|`+marketingAccepted+`
|Whether marketing messages are accepted
|===
We can include this snippet in AsciiDoc pages during the Maven build using the Asciidoctor plugin.
The main advantage is that our snippets come from requests that actually ran in tests. If tested behavior or descriptors no longer match, tests or snippet generation fail, forcing us to update code and documentation together.
7. Strict vs Relaxed Parameter Documentation
By default, formParameters(…) is strict. This means undocumented parameters fail the snippet unless we mark them as ignored.
However, sometimes we want lighter constraints. For example, we may only care about documenting core fields while allowing additional keys in the request. In that case, we enter the relaxed mode via relaxedFormParameters() instead of formParameters(). Let’s write a new helper method with relaxedFormParameters():
private FormParametersSnippet docRelaxedCoreFormParams() {
return relaxedFormParameters(
parameterWithName("email").description("The subscriber email address"),
parameterWithName("name").description("The display name of the subscriber"),
parameterWithName("frequency").description("Delivery frequency: weekly or monthly"));
}
Here’s the accompanying test method:
@Test
void whenOnlyCoreFieldsMatter_thenDocumentWithRelaxedMode() throws Exception {
mockMvc.perform(postSubscription())
.andExpect(status().isCreated())
.andDo(document("newsletter-subscribe-relaxed", docRelaxedCoreFormParams()));
}
It submits the form, checks if the return status is created, then generates the documentation snippets in the newsletter-subscribe-relaxed folder.
7.1. Quick Reference for Parameter Modes
Let’s review all parameter processing modes:
| Mode | How to use | What happens |
|---|---|---|
| Required parameter | parameterWithName(“email”) | Must be present in the documented request |
| Optional parameter | parameterWithName(“topics”).optional() | We can omit it without snippet failure |
| Ignored parameter | parameterWithName(“trackingId”).ignored() | May be present but is excluded from published contract |
| Relaxed mode | relaxedFormParameters(…) | Tolerates undocumented parameters |
In practice, we’ll use strict mode for stable contracts, add ignored() for known non-contract inputs, and switch to the relaxed mode only when we need additional flexibility.
8. Conclusion
In this article, we documented a form-encoded Spring MVC endpoint using Spring REST Docs and a test-first workflow. With Spring REST Docs, we document tested behavior and regenerate snippets as part of the test cycle, which helps avoid documentation drift over time.
We created a dedicated form model, validated input fields, and exposed a POST endpoint that consumes application/x-www-form-urlencoded requests. In the end, we used formParameters() to describe each key, including optional and ignored fields.
We covered required parameters, optional parameters, ignored parameters, and differences between strict and relaxed mode. Also, we saw how to define where REST Docs saves the documentation snippets.
As always, the source code is available over on GitHub.
















