Update field snippets to no longer document whole subsection by default
Previously, when a field was documented it would implicitly document the whole subsection of the payload identified by that field. This could lead to users inadvertently failing to document part of the payload. Arguably, this was a bug as it violated REST Docs' principle of producing accurate, detail documentation. However, fixing it requires a breaking change as people may also be relying on this behaviour. A balance needed to be struck so the fix is being made in a minor release. This commit introduces a new subsectionWithPath method which returns a SubsectionDescriptor; a specialisation of FieldDescriptor. Users that were intentionally relying on the old behaviour will have to replace some usage of fieldWithPath with subsectionWithPath instead. Users who were unintentionally relying on the old behaviour will have to add some additional descriptors produced using fieldWithPath and will receive more accurate documentation in return. Closes gh-274
This commit is contained in:
@@ -110,7 +110,19 @@ include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links]
|
||||
|
||||
In addition to the hypermedia-specific support <<documenting-your-api-hypermedia,described
|
||||
above>>, support for general documentation of request and response payloads is also
|
||||
provided. For example:
|
||||
provided. Consider the following payload:
|
||||
|
||||
[source,json,indent=0]
|
||||
----
|
||||
{
|
||||
"contact": {
|
||||
"name": "Jane Doe",
|
||||
"email": "jane.doe@example.com"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
It can be documented like this:
|
||||
|
||||
[source,java,indent=0,role="primary"]
|
||||
.MockMvc
|
||||
@@ -120,9 +132,9 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response]
|
||||
<1> Configure Spring REST docs to produce a snippet describing the fields in the response
|
||||
payload. To document a request `requestFields` can be used. Both are static methods on
|
||||
`org.springframework.restdocs.payload.PayloadDocumentation`.
|
||||
<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on
|
||||
`org.springframework.restdocs.payload.PayloadDocumentation`.
|
||||
<3> Expect a field with the path `contact.email`.
|
||||
<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method
|
||||
on `org.springframework.restdocs.payload.PayloadDocumentation`.
|
||||
<3> Expect a field with the path `contact.name`.
|
||||
|
||||
[source,java,indent=0,role="secondary"]
|
||||
.REST Assured
|
||||
@@ -132,9 +144,9 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=response]
|
||||
<1> Configure Spring REST docs to produce a snippet describing the fields in the response
|
||||
payload. To document a request `requestFields` can be used. Both are static methods on
|
||||
`org.springframework.restdocs.payload.PayloadDocumentation`.
|
||||
<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on
|
||||
<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method on
|
||||
`org.springframework.restdocs.payload.PayloadDocumentation`.
|
||||
<3> Expect a field with the path `contact.email`.
|
||||
<3> Expect a field with the path `contact.name`.
|
||||
|
||||
The result is a snippet that contains a table describing the fields. For requests this
|
||||
snippet is named `request-fields.adoc`. For responses this snippet is named
|
||||
@@ -142,12 +154,36 @@ snippet is named `request-fields.adoc`. For responses this snippet is named
|
||||
|
||||
When documenting fields, the test will fail if an undocumented field is found in the
|
||||
payload. Similarly, the test will also fail if a documented field is not found in the
|
||||
payload and the field has not been marked as optional. For payloads with a hierarchical
|
||||
structure, documenting a field is sufficient for all of its descendants to also be
|
||||
treated as having been documented.
|
||||
payload and the field has not been marked as optional.
|
||||
|
||||
If you do not want to document a field, you can mark it as ignored. This will prevent it
|
||||
from appearing in the generated snippet while avoiding the failure described above.
|
||||
If you don't want to provide detailed documentation for all of the fields, an entire
|
||||
subsection of a payload can be documented. For example:
|
||||
|
||||
[source,java,indent=0,role="primary"]
|
||||
.MockMvc
|
||||
----
|
||||
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
|
||||
----
|
||||
<1> Document the subsection with the path `contact`. `contact.email` and `contact.name`
|
||||
are now seen has having also been documented. Uses the static `subsectionWithPath`
|
||||
method on `org.springframework.restdocs.payload.PayloadDocumentation`.
|
||||
|
||||
[source,java,indent=0,role="secondary"]
|
||||
.REST Assured
|
||||
----
|
||||
include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection]
|
||||
----
|
||||
<1> Document the subsection with the path `contact`. `contact.email` and `contact.name`
|
||||
are now seen has having also been documented. Uses the static `subsectionWithPath`
|
||||
method on `org.springframework.restdocs.payload.PayloadDocumentation`.
|
||||
|
||||
`subsectionWithPath` can be useful for providing a high-level overview of a particular
|
||||
section of a payload. Separate, more detailed documentation for a subsection can then
|
||||
<<documenting-your-api-request-response-payloads-subsections, be produced>>.
|
||||
|
||||
If you do not want to document a field or subsection at all, you can mark it as ignored.
|
||||
This will prevent it from appearing in the generated snippet while avoiding the failure
|
||||
described above.
|
||||
|
||||
Fields can also be documented in a relaxed mode where any undocumented fields will not
|
||||
cause a test failure. To do so, use the `relaxedRequestFields` and `relaxedResponseFields`
|
||||
@@ -233,7 +269,7 @@ The following paths are all present:
|
||||
|
||||
|===
|
||||
|
||||
A response that uses an array at its root can also be documented. The path `[]` will refer
|
||||
A payload that uses an array at its root can also be documented. The path `[]` will refer
|
||||
to the entire array. You can then use bracket or dot notation to identify fields within
|
||||
the array's entries. For example, `[].id` corresponds to the `id` field of every object
|
||||
found in the following array:
|
||||
@@ -403,7 +439,7 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array]
|
||||
<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].`
|
||||
|
||||
[[documenting-your-api-request-response-payloads-subsections]]
|
||||
==== Documenting a portion of a request or response payload
|
||||
==== Documenting a subsection of a request or response payload
|
||||
|
||||
If a payload is large or structurally complex, it can be useful to document
|
||||
individual sections of the payload. REST Docs allows you to do so by extracting a
|
||||
@@ -433,7 +469,7 @@ be produced as follows:
|
||||
[source,java,indent=0,role="primary"]
|
||||
.MockMvc
|
||||
----
|
||||
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
|
||||
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path]
|
||||
----
|
||||
<1> Produce a snippet describing the fields in the subsection of the response payload
|
||||
beneath the path `weather.temperature`. Uses the static `beneathPath` method on
|
||||
@@ -443,7 +479,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
|
||||
[source,java,indent=0,role="secondary"]
|
||||
.REST Assured
|
||||
----
|
||||
include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection]
|
||||
include::{examples-dir}/com/example/restassured/Payload.java[tags=beneath-path]
|
||||
----
|
||||
<1> Produce a snippet describing the fields in the subsection of the response payload
|
||||
beneath the path `weather.temperature`. Uses the static `beneathPath` method on
|
||||
@@ -457,6 +493,7 @@ example, the code above will result in a snippet named
|
||||
`response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using
|
||||
the `withSubsectionId(String)` method:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id]
|
||||
----
|
||||
|
||||
@@ -26,6 +26,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuild
|
||||
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.snippet.Attributes.attributes;
|
||||
@@ -41,10 +42,19 @@ public class Payload {
|
||||
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andDo(document("index", responseFields( // <1>
|
||||
fieldWithPath("contact").description("The user's contact details"), // <2>
|
||||
fieldWithPath("contact.email").description("The user's email address")))); // <3>
|
||||
fieldWithPath("contact.email").description("The user's email address"), // <2>
|
||||
fieldWithPath("contact.name").description("The user's name")))); // <3>
|
||||
// end::response[]
|
||||
}
|
||||
|
||||
public void subsection() throws Exception {
|
||||
// tag::subsection[]
|
||||
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andDo(document("index", responseFields( // <1>
|
||||
subsectionWithPath("contact").description("The user's contact details")))); // <1>
|
||||
// end::subsection[]
|
||||
}
|
||||
|
||||
public void explicitType() throws Exception {
|
||||
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
|
||||
@@ -92,14 +102,14 @@ public class Payload {
|
||||
// end::book-array[]
|
||||
}
|
||||
|
||||
public void subsection() throws Exception {
|
||||
// tag::subsection[]
|
||||
public void subsectionBeneathPath() throws Exception {
|
||||
// tag::beneath-path[]
|
||||
this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1>
|
||||
fieldWithPath("high").description("The forecast high in degrees celcius"), // <2>
|
||||
fieldWithPath("low").description("The forecast low in degrees celcius"))));
|
||||
// end::subsection[]
|
||||
// end::beneath-path[]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathP
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
|
||||
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document;
|
||||
import static org.springframework.restdocs.snippet.Attributes.attributes;
|
||||
import static org.springframework.restdocs.snippet.Attributes.key;
|
||||
@@ -39,12 +40,22 @@ public class Payload {
|
||||
// tag::response[]
|
||||
RestAssured.given(this.spec).accept("application/json")
|
||||
.filter(document("user", responseFields( // <1>
|
||||
fieldWithPath("contact").description("The user's contact details"), // <2>
|
||||
fieldWithPath("contact.name").description("The user's name"), // <2>
|
||||
fieldWithPath("contact.email").description("The user's email address")))) // <3>
|
||||
.when().get("/user/5")
|
||||
.then().assertThat().statusCode(is(200));
|
||||
// end::response[]
|
||||
}
|
||||
|
||||
public void subsection() throws Exception {
|
||||
// tag::subsection[]
|
||||
RestAssured.given(this.spec).accept("application/json")
|
||||
.filter(document("user", responseFields(
|
||||
subsectionWithPath("contact").description("The user's contact details")))) // <1>
|
||||
.when().get("/user/5")
|
||||
.then().assertThat().statusCode(is(200));
|
||||
// end::response[]
|
||||
}
|
||||
|
||||
public void explicitType() throws Exception {
|
||||
RestAssured.given(this.spec).accept("application/json")
|
||||
@@ -96,15 +107,15 @@ public class Payload {
|
||||
// end::book-array[]
|
||||
}
|
||||
|
||||
public void subsection() throws Exception {
|
||||
// tag::subsection[]
|
||||
public void subsectionBeneathPath() throws Exception {
|
||||
// tag::beneath-path[]
|
||||
RestAssured.given(this.spec).accept("application/json")
|
||||
.filter(document("location", responseFields(beneathPath("weather.temperature"), // <1>
|
||||
fieldWithPath("high").description("The forecast high in degrees celcius"), // <2>
|
||||
fieldWithPath("low").description("The forecast low in degrees celcius"))))
|
||||
.when().get("/locations/1")
|
||||
.then().assertThat().statusCode(is(200));
|
||||
// end::subsection[]
|
||||
// end::beneath-path[]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user