diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 908a5fd8..02a5d697 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -81,12 +81,18 @@ payload and the field has not been marked as optional. For payloads with a hiera structure, documenting a field is sufficient for all of its descendants to also be treated as having been documented. -[[documenting-your-api-request-response-payloads-field-paths]] -==== Field paths +TIP: By default, Spring REST Docs will assume that the payload you are documenting is +JSON. If you want to document an XML payload the content type of the request or response +must be compatible with `application/xml`. -When documenting request and response payloads, fields are identified using a path. Paths -use `.` to descend into a child object and `[]` to identify an array. For example, with -this JSON payload: +[[documenting-your-api-request-response-payloads-json]] +==== JSON payloads + +[[documenting-your-api-request-response-payloads-json-field-paths]] +===== JSON field paths + +JSON field paths use `.` to descend into a child object and `[]` to identify an array. For +example, with this JSON payload: [source,json,indent=0] ---- @@ -131,8 +137,8 @@ The following paths are all present: -[[documenting-your-api-request-response-payloads-field-types]] -==== Field types +[[documenting-your-api-request-response-payloads-json-field-types]] +===== JSON field types When a field is documented, Spring REST Docs will attempt to determine its type by examining the payload. Seven different types are supported: @@ -163,8 +169,9 @@ examining the payload. Seven different types are supported: | The field occurs multiple times in the payload with a variety of different types |=== -The type can also be set explicitly using the `type(FieldType)` method on -`FieldDescriptor`: +The type can also be set explicitly using the `type(Object)` method on +`FieldDescriptor`. Typically, one of the values enumerated by `JsonFieldType` will be +used: [source,java,indent=0] ---- @@ -173,6 +180,24 @@ include::{examples-dir}/com/example/Payload.java[tags=explicit-type] <1> Set the field's type to `string`. +[[documenting-your-api-request-response-payloads-xml]] +==== XML payloads + +[[documenting-your-api-request-response-payloads-xml-field-paths]] +===== XML field paths + +XML field paths are described using XPath. `/` is used to descend into a child node. + + + +[[documenting-your-api-request-response-payloads-xml-field-types]] +===== XML field types + +When documenting an XML payload, you must provide a type for the field using the +`type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` +method will be included in the documentation. + + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index a317ec1c..1ca668b8 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -27,7 +27,7 @@ import static org.springframework.restdocs.RestDocumentationRequestBuilders.post import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; -import org.springframework.restdocs.payload.FieldType; +import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; public class Payload { @@ -50,7 +50,7 @@ private MockMvc mockMvc; // tag::explicit-type[] .andDo(document("index", responseFields( fieldWithPath("contact.email") - .type(FieldType.STRING) // <1> + .type(JsonFieldType.STRING) // <1> .optional() .description("The user's email address")))); // end::explicit-type[] diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index d9dd6df1..b8d4aa2d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -18,36 +18,28 @@ package org.springframework.restdocs.payload; import java.io.IOException; import java.io.Reader; +import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; - -import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; /** - * A {@link TemplatedSnippet} that produces a snippet documenting a - * RESTful resource's request or response fields. + * A {@link TemplatedSnippet} that produces a snippet documenting a RESTful resource's + * request or response fields. * * @author Andreas Evers * @author Andy Wilkinson */ -public abstract class AbstractFieldsSnippet extends - TemplatedSnippet { - - private final Map descriptorsByPath = new LinkedHashMap(); - - private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver(); - - private final FieldValidator fieldValidator = new FieldValidator(); - - private final ObjectMapper objectMapper = new ObjectMapper(); +public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private List fieldDescriptors; @@ -57,45 +49,73 @@ public abstract class AbstractFieldsSnippet extends for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); Assert.hasText(descriptor.getDescription()); - this.descriptorsByPath.put(descriptor.getPath(), descriptor); } this.fieldDescriptors = descriptors; } @Override protected Map document(MvcResult result) throws IOException { - this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); - Object payload = extractPayload(result); + MediaType contentType = getContentType(result); + PayloadHandler payloadHandler; + if (contentType != null + && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { + payloadHandler = new XmlPayloadHandler(readPayload(result)); + } + else { + payloadHandler = new JsonPayloadHandler(readPayload(result)); + } + + validateFieldDocumentation(payloadHandler); + + for (FieldDescriptor descriptor : this.fieldDescriptors) { + if (descriptor.getType() == null) { + descriptor.type(payloadHandler.determineFieldType(descriptor.getPath())); + } + } + Map model = new HashMap<>(); List> fields = new ArrayList<>(); model.put("fields", fields); - for (Entry entry : this.descriptorsByPath.entrySet()) { - FieldDescriptor descriptor = entry.getValue(); - if (descriptor.getType() == null) { - descriptor.type(getFieldType(descriptor, payload)); - } + for (FieldDescriptor descriptor : this.fieldDescriptors) { fields.add(descriptor.toModel()); } return model; } - private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { - try { - return AbstractFieldsSnippet.this.fieldTypeResolver - .resolveFieldType(descriptor.getPath(), payload); - } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the" - + " payload. Please provide a type using" - + " FieldDescriptor.type(FieldType)."; - throw new FieldTypeRequiredException(message); + private String readPayload(MvcResult result) throws IOException { + StringWriter writer = new StringWriter(); + FileCopyUtils.copy(getPayloadReader(result), writer); + return writer.toString(); + } + + private void validateFieldDocumentation(PayloadHandler payloadHandler) { + List missingFields = payloadHandler + .findMissingFields(this.fieldDescriptors); + String undocumentedPayload = payloadHandler + .getUndocumentedPayload(this.fieldDescriptors); + + if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) { + String message = ""; + if (StringUtils.hasText(undocumentedPayload)) { + message += String.format("The following parts of the payload were" + + " not documented:%n%s", undocumentedPayload); + } + if (!missingFields.isEmpty()) { + if (message.length() > 0) { + message += String.format("%n"); + } + List paths = new ArrayList(); + for (FieldDescriptor fieldDescriptor : missingFields) { + paths.add(fieldDescriptor.getPath()); + } + message += "Fields with the following paths were not found in the" + + " payload: " + paths; + } + throw new SnippetException(message); } } - private Object extractPayload(MvcResult result) throws IOException { - return this.objectMapper.readValue(getPayloadReader(result), Object.class); - } + protected abstract MediaType getContentType(MvcResult result); protected abstract Reader getPayloadReader(MvcResult result) throws IOException; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 0a14327e..5cbf6384 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -33,7 +33,7 @@ public class FieldDescriptor extends AbstractDescriptor { private final String path; - private FieldType type; + private Object type; private boolean optional; @@ -44,13 +44,14 @@ public class FieldDescriptor extends AbstractDescriptor { } /** - * Specifies the type of the field + * Specifies the type of the field. When documenting a JSON payload, the + * {@link JsonFieldType} enumeration will typically be used. * * @param type The type of the field - * * @return {@code this} + * @see JsonFieldType */ - public FieldDescriptor type(FieldType type) { + public FieldDescriptor type(Object type) { this.type = type; return this; } @@ -80,7 +81,7 @@ public class FieldDescriptor extends AbstractDescriptor { return this.path; } - FieldType getType() { + Object getType() { return this.type; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java index 99f565e2..e2cf4906 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java @@ -31,7 +31,7 @@ public class FieldDoesNotExistException extends RuntimeException { * * @param fieldPath the path of the field that does not exist */ - public FieldDoesNotExistException(FieldPath fieldPath) { + public FieldDoesNotExistException(JsonFieldPath fieldPath) { super("The payload does not contain a field with the path '" + fieldPath + "'"); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java deleted file mode 100644 index 82e43717..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2014-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.payload; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.springframework.restdocs.snippet.SnippetException; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -/** - * {@code FieldValidator} is used to validate a payload's fields against the user-provided - * {@link FieldDescriptor}s. - * - * @author Andy Wilkinson - */ -class FieldValidator { - - private final FieldProcessor fieldProcessor = new FieldProcessor(); - - private final ObjectMapper objectMapper = new ObjectMapper() - .enable(SerializationFeature.INDENT_OUTPUT); - - void validate(Reader payloadReader, List fieldDescriptors) - throws IOException { - Object payload = this.objectMapper.readValue(payloadReader, Object.class); - List missingFields = findMissingFields(payload, fieldDescriptors); - Object undocumentedPayload = findUndocumentedFields(payload, fieldDescriptors); - - if (!missingFields.isEmpty() || !isEmpty(undocumentedPayload)) { - String message = ""; - if (!isEmpty(undocumentedPayload)) { - message += String.format( - "The following parts of the payload were not documented:%n%s", - this.objectMapper.writeValueAsString(undocumentedPayload)); - } - if (!missingFields.isEmpty()) { - if (message.length() > 0) { - message += String.format("%n"); - } - message += "Fields with the following paths were not found in the payload: " - + missingFields; - } - throw new SnippetException(message); - } - } - - private boolean isEmpty(Object object) { - if (object instanceof Map) { - return ((Map) object).isEmpty(); - } - return ((List) object).isEmpty(); - } - - private List findMissingFields(Object payload, - List fieldDescriptors) { - List missingFields = new ArrayList(); - - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - if (!fieldDescriptor.isOptional() - && !this.fieldProcessor.hasField( - FieldPath.compile(fieldDescriptor.getPath()), payload)) { - missingFields.add(fieldDescriptor.getPath()); - } - } - - return missingFields; - } - - private Object findUndocumentedFields(Object payload, - List fieldDescriptors) { - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - FieldPath path = FieldPath.compile(fieldDescriptor.getPath()); - this.fieldProcessor.remove(path, payload); - } - return payload; - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java similarity index 90% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index a1dfb033..ea44aaff 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -22,12 +22,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * A path that identifies a field in a payload + * A path that identifies a field in a JSON payload * * @author Andy Wilkinson * */ -final class FieldPath { +final class JsonFieldPath { private static final Pattern ARRAY_INDEX_PATTERN = Pattern .compile("\\[([0-9]+|\\*){0,1}\\]"); @@ -38,7 +38,7 @@ final class FieldPath { private final boolean precise; - private FieldPath(String rawPath, List segments, boolean precise) { + private JsonFieldPath(String rawPath, List segments, boolean precise) { this.rawPath = rawPath; this.segments = segments; this.precise = precise; @@ -57,9 +57,9 @@ final class FieldPath { return this.rawPath; } - static FieldPath compile(String path) { + static JsonFieldPath compile(String path) { List segments = extractSegments(path); - return new FieldPath(path, segments, matchesSingleValue(segments)); + return new JsonFieldPath(path, segments, matchesSingleValue(segments)); } static boolean isArraySegment(String segment) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java similarity index 90% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index f0e3d25d..e1aa72d6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -23,15 +23,15 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; /** - * A {@code FieldProcessor} processes a payload's fields, allowing them to be extracted - * and removed + * A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be + * extracted and removed * * @author Andy Wilkinson * */ -final class FieldProcessor { +final class JsonFieldProcessor { - boolean hasField(FieldPath fieldPath, Object payload) { + boolean hasField(JsonFieldPath fieldPath, Object payload) { final AtomicReference hasField = new AtomicReference(false); traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() { @@ -44,7 +44,7 @@ final class FieldProcessor { return hasField.get(); } - Object extract(FieldPath path, Object payload) { + Object extract(JsonFieldPath path, Object payload) { final List matches = new ArrayList(); traverse(new ProcessingContext(payload, path), new MatchCallback() { @@ -65,7 +65,7 @@ final class FieldProcessor { } } - void remove(final FieldPath path, Object payload) { + void remove(final JsonFieldPath path, Object payload) { traverse(new ProcessingContext(payload, path), new MatchCallback() { @Override @@ -78,7 +78,7 @@ final class FieldProcessor { private void traverse(ProcessingContext context, MatchCallback matchCallback) { final String segment = context.getSegment(); - if (FieldPath.isArraySegment(segment)) { + if (JsonFieldPath.isArraySegment(segment)) { if (context.getPayload() instanceof List) { handleListPayload(context, matchCallback); } @@ -206,13 +206,13 @@ final class FieldProcessor { private final Match parent; - private final FieldPath path; + private final JsonFieldPath path; - private ProcessingContext(Object payload, FieldPath path) { + private ProcessingContext(Object payload, JsonFieldPath path) { this(payload, path, null, null); } - private ProcessingContext(Object payload, FieldPath path, List segments, + private ProcessingContext(Object payload, JsonFieldPath path, List segments, Match parent) { this.payload = payload; this.path = path; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java similarity index 97% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java index 7eca8684..0e3b287a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java @@ -25,7 +25,7 @@ import org.springframework.util.StringUtils; * * @author Andy Wilkinson */ -public enum FieldType { +public enum JsonFieldType { ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java similarity index 66% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java index dff50cda..ca55dac5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java @@ -20,26 +20,26 @@ import java.util.Collection; import java.util.Map; /** - * Resolves the type of a field in a request or response payload + * Resolves the type of a field in a JSON request or response payload * * @author Andy Wilkinson */ -class FieldTypeResolver { +class JsonFieldTypeResolver { - private final FieldProcessor fieldProcessor = new FieldProcessor(); + private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); - FieldType resolveFieldType(String path, Object payload) { - FieldPath fieldPath = FieldPath.compile(path); + JsonFieldType resolveFieldType(String path, Object payload) { + JsonFieldPath fieldPath = JsonFieldPath.compile(path); Object field = this.fieldProcessor.extract(fieldPath, payload); if (field instanceof Collection && !fieldPath.isPrecise()) { - FieldType commonType = null; + JsonFieldType commonType = null; for (Object item : (Collection) field) { - FieldType fieldType = determineFieldType(item); + JsonFieldType fieldType = determineFieldType(item); if (commonType == null) { commonType = fieldType; } else if (fieldType != commonType) { - return FieldType.VARIES; + return JsonFieldType.VARIES; } } return commonType; @@ -47,22 +47,22 @@ class FieldTypeResolver { return determineFieldType(this.fieldProcessor.extract(fieldPath, payload)); } - private FieldType determineFieldType(Object fieldValue) { + private JsonFieldType determineFieldType(Object fieldValue) { if (fieldValue == null) { - return FieldType.NULL; + return JsonFieldType.NULL; } if (fieldValue instanceof String) { - return FieldType.STRING; + return JsonFieldType.STRING; } if (fieldValue instanceof Map) { - return FieldType.OBJECT; + return JsonFieldType.OBJECT; } if (fieldValue instanceof Collection) { - return FieldType.ARRAY; + return JsonFieldType.ARRAY; } if (fieldValue instanceof Boolean) { - return FieldType.BOOLEAN; + return JsonFieldType.BOOLEAN; } - return FieldType.NUMBER; + return JsonFieldType.NUMBER; } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java new file mode 100644 index 00000000..93c824cb --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java @@ -0,0 +1,92 @@ +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * A {@link PayloadHandler} for JSON payloads + * + * @author Andy Wilkinson + */ +class JsonPayloadHandler implements PayloadHandler { + + private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); + + private final ObjectMapper objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + private final String rawPayload; + + JsonPayloadHandler(String payload) throws IOException { + this.rawPayload = payload; + } + + @Override + public List findMissingFields(List fieldDescriptors) { + List missingFields = new ArrayList(); + Object payload = readPayload(); + for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + if (!fieldDescriptor.isOptional() + && !this.fieldProcessor.hasField( + JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) { + missingFields.add(fieldDescriptor); + } + } + + return missingFields; + } + + @Override + public String getUndocumentedPayload(List fieldDescriptors) { + Object payload = readPayload(); + for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath()); + this.fieldProcessor.remove(path, payload); + } + if (!isEmpty(payload)) { + try { + return this.objectMapper.writeValueAsString(payload); + } + catch (JsonProcessingException ex) { + throw new PayloadHandlingException(ex); + } + } + return null; + } + + private Object readPayload() { + try { + return new ObjectMapper().readValue(this.rawPayload, Object.class); + } + catch (IOException ex) { + throw new PayloadHandlingException(ex); + } + } + + private boolean isEmpty(Object object) { + if (object instanceof Map) { + return ((Map) object).isEmpty(); + } + return ((List) object).isEmpty(); + } + + @Override + public Object determineFieldType(String path) { + try { + return new JsonFieldTypeResolver().resolveFieldType(path, readPayload()); + } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + path + "' as" + + " it is not present in the payload. Please provide a type using" + + " FieldDescriptor.type(Object type)."; + throw new FieldTypeRequiredException(message); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 40e04704..72b0494a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -37,8 +37,12 @@ public abstract class PayloadDocumentation { * Creates a {@code FieldDescriptor} that describes a field with the given * {@code path}. *

- * The {@code path} uses '.' to descend into a child object and ' {@code []}' to - * descend into an array. For example, with this JSON payload: + * When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to + * descend to a child node. + *

+ * When documenting a JSON payload, the {@code path} uses '.' to descend into a child + * object and ' {@code []}' to descend into an array. For example, with this JSON + * payload: * *

 	 * {
@@ -132,8 +136,7 @@ public abstract class PayloadDocumentation {
 	 */
 	public static Snippet requestFields(Map attributes,
 			FieldDescriptor... descriptors) {
-		return new RequestFieldsSnippet(attributes,
-				Arrays.asList(descriptors));
+		return new RequestFieldsSnippet(attributes, Arrays.asList(descriptors));
 	}
 
 	/**
@@ -174,8 +177,7 @@ public abstract class PayloadDocumentation {
 	 */
 	public static Snippet responseFields(Map attributes,
 			FieldDescriptor... descriptors) {
-		return new ResponseFieldsSnippet(attributes,
-				Arrays.asList(descriptors));
+		return new ResponseFieldsSnippet(attributes, Arrays.asList(descriptors));
 	}
 
 }
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java
new file mode 100644
index 00000000..5d6c2d47
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java
@@ -0,0 +1,45 @@
+package org.springframework.restdocs.payload;
+
+import java.util.List;
+
+/**
+ * A handler for a request or response payload
+ * 
+ * @author Andy Wilkinson
+ */
+interface PayloadHandler {
+
+	/**
+	 * Finds the fields that are missing from the handler's payload. A field is missing if
+	 * it is described by one of the {@code fieldDescriptors} but is not present in the
+	 * payload.
+	 * 
+	 * @param fieldDescriptors the descriptors
+	 * @return descriptors for the fields that are missing from the payload
+	 * @throws PayloadHandlingException if a failure occurs
+	 */
+	List findMissingFields(List fieldDescriptors);
+
+	/**
+	 * Returns a modified payload, formatted as a String, that only contains the fields
+	 * that are undocumented. A field is undocumented if it is present in the handler's
+	 * payload but is not described by the given {@code fieldDescriptors}. If the payload
+	 * is completely documented, {@code null} is returned
+	 * 
+	 * @param fieldDescriptors the descriptors
+	 * @return the undocumented payload, or {@code null} if all of the payload is
+	 * documented
+	 * @throws PayloadHandlingException if a failure occurs
+	 */
+	String getUndocumentedPayload(List fieldDescriptors);
+
+	/**
+	 * Returns the type of the field with the given {@code path} based on the content of
+	 * the payload.
+	 * 
+	 * @param path the field path
+	 * @return the type of the field
+	 */
+	Object determineFieldType(String path);
+
+}
\ No newline at end of file
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java
new file mode 100644
index 00000000..a569b39b
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java
@@ -0,0 +1,20 @@
+package org.springframework.restdocs.payload;
+
+/**
+ * Thrown to indicate that a failure has occurred during payload handling
+ * 
+ * @author Andy Wilkinson
+ *
+ */
+@SuppressWarnings("serial")
+class PayloadHandlingException extends RuntimeException {
+
+	/**
+	 * Creates a new {@code PayloadHandlingException} with the given cause
+	 * @param cause the cause of the failure
+	 */
+	PayloadHandlingException(Throwable cause) {
+		super(cause);
+	}
+
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java
index bff0832f..c7e25c9b 100644
--- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java
@@ -20,6 +20,7 @@ import java.io.Reader;
 import java.util.List;
 import java.util.Map;
 
+import org.springframework.http.MediaType;
 import org.springframework.restdocs.snippet.Snippet;
 import org.springframework.test.web.servlet.MvcResult;
 
@@ -43,4 +44,13 @@ class RequestFieldsSnippet extends AbstractFieldsSnippet {
 		return result.getRequest().getReader();
 	}
 
+	@Override
+	protected MediaType getContentType(MvcResult result) {
+		String contentType = result.getRequest().getContentType();
+		if (contentType != null) {
+			return MediaType.valueOf(contentType);
+		}
+		return null;
+	}
+
 }
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java
index 2972d80d..440e5b71 100644
--- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java
@@ -21,11 +21,12 @@ import java.io.StringReader;
 import java.util.List;
 import java.util.Map;
 
+import org.springframework.http.MediaType;
 import org.springframework.restdocs.snippet.Snippet;
 import org.springframework.test.web.servlet.MvcResult;
 
 /**
- * A {@link Snippet} the documents the fields in a response.
+ * A {@link Snippet} that documents the fields in a response.
  * 
  * @author Andy Wilkinson
  */
@@ -45,4 +46,13 @@ class ResponseFieldsSnippet extends AbstractFieldsSnippet {
 		return new StringReader(result.getResponse().getContentAsString());
 	}
 
+	@Override
+	protected MediaType getContentType(MvcResult result) {
+		String contentType = result.getResponse().getContentType();
+		if (contentType != null) {
+			return MediaType.valueOf(contentType);
+		}
+		return null;
+	}
+
 }
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java
new file mode 100644
index 00000000..c50aaba0
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java
@@ -0,0 +1,141 @@
+package org.springframework.restdocs.payload;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+/**
+ * A {@link PayloadHandler} for XML payloads
+ * 
+ * @author Andy Wilkinson
+ */
+class XmlPayloadHandler implements PayloadHandler {
+
+	private final DocumentBuilder documentBuilder;
+
+	private final String rawPayload;
+
+	XmlPayloadHandler(String rawPayload) {
+		try {
+			this.documentBuilder = DocumentBuilderFactory.newInstance()
+					.newDocumentBuilder();
+		}
+		catch (ParserConfigurationException ex) {
+			throw new IllegalStateException("Failed to create document builder", ex);
+		}
+		this.rawPayload = rawPayload;
+	}
+
+	@Override
+	public List findMissingFields(List fieldDescriptors) {
+		List missingFields = new ArrayList<>();
+		Document payload = readPayload();
+		for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
+			if (!fieldDescriptor.isOptional()) {
+				NodeList matchingNodes = findMatchingNodes(fieldDescriptor, payload);
+				if (matchingNodes.getLength() == 0) {
+					missingFields.add(fieldDescriptor);
+				}
+			}
+		}
+
+		return missingFields;
+	}
+
+	private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document payload) {
+		try {
+			return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload,
+					XPathConstants.NODESET);
+		}
+		catch (XPathExpressionException ex) {
+			throw new PayloadHandlingException(ex);
+		}
+	}
+
+	private Document readPayload() {
+		try {
+			return this.documentBuilder.parse(new InputSource(new StringReader(
+					this.rawPayload)));
+		}
+		catch (Exception ex) {
+			throw new PayloadHandlingException(ex);
+		}
+	}
+
+	private XPathExpression createXPath(String fieldPath) throws XPathExpressionException {
+		return XPathFactory.newInstance().newXPath().compile(fieldPath);
+	}
+
+	@Override
+	public String getUndocumentedPayload(List fieldDescriptors) {
+		Document payload = readPayload();
+		for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
+			NodeList matchingNodes;
+			try {
+				matchingNodes = (NodeList) createXPath(fieldDescriptor.getPath())
+						.evaluate(payload, XPathConstants.NODESET);
+			}
+			catch (XPathExpressionException ex) {
+				throw new PayloadHandlingException(ex);
+			}
+			for (int i = 0; i < matchingNodes.getLength(); i++) {
+				Node node = matchingNodes.item(i);
+				node.getParentNode().removeChild(node);
+			}
+		}
+		if (payload.getChildNodes().getLength() > 0) {
+			return prettyPrint(payload);
+		}
+		return null;
+	}
+
+	private String prettyPrint(Document document) {
+		try {
+			StringWriter stringWriter = new StringWriter();
+			StreamResult xmlOutput = new StreamResult(stringWriter);
+			TransformerFactory transformerFactory = TransformerFactory.newInstance();
+			transformerFactory.setAttribute("indent-number", 4);
+			Transformer transformer = transformerFactory.newTransformer();
+			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+			transformer.transform(new DOMSource(document), xmlOutput);
+			return xmlOutput.getWriter().toString();
+		}
+		catch (Exception ex) {
+			throw new PayloadHandlingException(ex);
+		}
+	}
+
+	@Override
+	public Object determineFieldType(String path) {
+		try {
+			return new JsonFieldTypeResolver().resolveFieldType(path, readPayload());
+		}
+		catch (FieldDoesNotExistException ex) {
+			String message = "Cannot determine the type of the field '" + path + "' as"
+					+ " it is not present in the payload. Please provide a type using"
+					+ " FieldDescriptor.type(Object type).";
+			throw new FieldTypeRequiredException(message);
+		}
+	}
+
+}
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java
deleted file mode 100644
index 53f118f4..00000000
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright 2014-2015 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.restdocs.payload;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.Arrays;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.restdocs.snippet.SnippetException;
-
-/**
- * Tests for {@link FieldValidator}
- * 
- * @author Andy Wilkinson
- */
-public class FieldValidatorTests {
-
-	private final FieldValidator fieldValidator = new FieldValidator();
-
-	@Rule
-	public ExpectedException thrownException = ExpectedException.none();
-
-	private StringReader listPayload = new StringReader(
-			"[{\"a\":1},{\"a\":2},{\"b\":{\"c\":3}}]");
-
-	private StringReader payload = new StringReader(
-			"{\"a\":{\"b\":{},\"c\":true,\"d\":[{\"e\":1},{\"e\":2}]}}");
-
-	@Test
-	public void noMissingFieldsAllFieldsDocumented() throws IOException {
-		this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor(
-				"a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e"),
-				new FieldDescriptor("a.d"), new FieldDescriptor("a")));
-	}
-
-	@Test
-	public void optionalFieldsAreNotReportedMissing() throws IOException {
-		this.fieldValidator.validate(this.payload, Arrays.asList(
-				new FieldDescriptor("a"), new FieldDescriptor("a.b"),
-				new FieldDescriptor("a.c"), new FieldDescriptor("a.d"),
-				new FieldDescriptor("y").optional()));
-	}
-
-	@Test
-	public void parentIsDocumentedWhenAllChildrenAreDocumented() throws IOException {
-		this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor(
-				"a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e")));
-	}
-
-	@Test
-	public void childIsDocumentedWhenParentIsDocumented() throws IOException {
-		this.fieldValidator.validate(this.payload,
-				Arrays.asList(new FieldDescriptor("a")));
-	}
-
-	@Test
-	public void missingField() throws IOException {
-		this.thrownException.expect(SnippetException.class);
-		this.thrownException
-				.expectMessage(equalTo("Fields with the following paths were not found"
-						+ " in the payload: [y, z]"));
-		this.fieldValidator.validate(this.payload, Arrays.asList(
-				new FieldDescriptor("a"), new FieldDescriptor("a.b"),
-				new FieldDescriptor("y"), new FieldDescriptor("z")));
-	}
-
-	@Test
-	public void undocumentedField() throws IOException {
-		this.thrownException.expect(SnippetException.class);
-		this.thrownException.expectMessage(equalTo(String
-				.format("The following parts of the payload were not"
-						+ " documented:%n{%n  \"a\" : {%n    \"c\" : true%n  }%n}")));
-		this.fieldValidator.validate(this.payload,
-				Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.d")));
-	}
-
-	@Test
-	public void listPayloadNoMissingFieldsAllFieldsDocumented() throws IOException {
-		this.fieldValidator.validate(this.listPayload, Arrays.asList(new FieldDescriptor(
-				"[]b.c"), new FieldDescriptor("[]b"), new FieldDescriptor("[]a"),
-				new FieldDescriptor("[]")));
-	}
-
-	@Test
-	public void listPayloadParentIsDocumentedWhenAllChildrenAreDocumented()
-			throws IOException {
-		this.fieldValidator.validate(this.listPayload,
-				Arrays.asList(new FieldDescriptor("[]b.c"), new FieldDescriptor("[]a")));
-	}
-
-	@Test
-	public void listPayloadChildIsDocumentedWhenParentIsDocumented() throws IOException {
-		this.fieldValidator.validate(this.listPayload,
-				Arrays.asList(new FieldDescriptor("[]")));
-	}
-}
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java
similarity index 67%
rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java
rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java
index 0e185720..f94beee4 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java
@@ -24,89 +24,89 @@ import static org.junit.Assert.assertTrue;
 import org.junit.Test;
 
 /**
- * Tests for {@link FieldPath}
+ * Tests for {@link JsonFieldPath}
  * 
  * @author Andy Wilkinson
  */
-public class FieldPathTests {
+public class JsonFieldPathTests {
 
 	@Test
 	public void singleFieldIsPrecise() {
-		assertTrue(FieldPath.compile("a").isPrecise());
+		assertTrue(JsonFieldPath.compile("a").isPrecise());
 	}
 
 	@Test
 	public void singleNestedFieldIsPrecise() {
-		assertTrue(FieldPath.compile("a.b").isPrecise());
+		assertTrue(JsonFieldPath.compile("a.b").isPrecise());
 	}
 
 	@Test
 	public void topLevelArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("[]").isPrecise());
+		assertFalse(JsonFieldPath.compile("[]").isPrecise());
 	}
 
 	@Test
 	public void fieldBeneathTopLevelArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("[]a").isPrecise());
+		assertFalse(JsonFieldPath.compile("[]a").isPrecise());
 	}
 
 	@Test
 	public void arrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("a[]").isPrecise());
+		assertFalse(JsonFieldPath.compile("a[]").isPrecise());
 	}
 
 	@Test
 	public void nestedArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("a.b[]").isPrecise());
+		assertFalse(JsonFieldPath.compile("a.b[]").isPrecise());
 	}
 
 	@Test
 	public void arrayOfArraysIsNotPrecise() {
-		assertFalse(FieldPath.compile("a[][]").isPrecise());
+		assertFalse(JsonFieldPath.compile("a[][]").isPrecise());
 	}
 
 	@Test
 	public void fieldBeneathAnArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("a[].b").isPrecise());
+		assertFalse(JsonFieldPath.compile("a[].b").isPrecise());
 	}
 
 	@Test
 	public void compilationOfSingleElementPath() {
-		assertThat(FieldPath.compile("a").getSegments(), contains("a"));
+		assertThat(JsonFieldPath.compile("a").getSegments(), contains("a"));
 	}
 
 	@Test
 	public void compilationOfMultipleElementPath() {
-		assertThat(FieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c"));
+		assertThat(JsonFieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithNoDotSeparators() {
-		assertThat(FieldPath.compile("a[]b[]c").getSegments(),
+		assertThat(JsonFieldPath.compile("a[]b[]c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithPreAndPostDotSeparators() {
-		assertThat(FieldPath.compile("a.[].b.[].c").getSegments(),
+		assertThat(JsonFieldPath.compile("a.[].b.[].c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithPreDotSeparators() {
-		assertThat(FieldPath.compile("a.[]b.[]c").getSegments(),
+		assertThat(JsonFieldPath.compile("a.[]b.[]c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithPostDotSeparators() {
-		assertThat(FieldPath.compile("a[].b[].c").getSegments(),
+		assertThat(JsonFieldPath.compile("a[].b[].c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathStartingWithAnArray() {
-		assertThat(FieldPath.compile("[]a.b.c").getSegments(),
+		assertThat(JsonFieldPath.compile("[]a.b.c").getSegments(),
 				contains("[]", "a", "b", "c"));
 	}
 
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java
similarity index 80%
rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java
rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java
index da71af5e..bc61a338 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java
@@ -30,19 +30,19 @@ import org.junit.Test;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 /**
- * Tests for {@link FieldProcessor}
+ * Tests for {@link JsonFieldProcessor}
  * 
  * @author Andy Wilkinson
  */
-public class FieldProcessorTests {
+public class JsonFieldProcessorTests {
 
-	private final FieldProcessor fieldProcessor = new FieldProcessor();
+	private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
 
 	@Test
 	public void extractTopLevelMapEntry() {
 		Map payload = new HashMap<>();
 		payload.put("a", "alpha");
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload),
 				equalTo((Object) "alpha"));
 	}
 
@@ -52,7 +52,7 @@ public class FieldProcessorTests {
 		Map alpha = new HashMap<>();
 		payload.put("a", alpha);
 		alpha.put("b", "bravo");
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a.b"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload),
 				equalTo((Object) "bravo"));
 	}
 
@@ -63,7 +63,7 @@ public class FieldProcessorTests {
 		bravo.put("b", "bravo");
 		List> alpha = Arrays.asList(bravo, bravo);
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload),
 				equalTo((Object) alpha));
 	}
 
@@ -74,7 +74,7 @@ public class FieldProcessorTests {
 		bravo.put("b", "bravo");
 		List> alpha = Arrays.asList(bravo, bravo);
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[]"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload),
 				equalTo((Object) alpha));
 	}
 
@@ -85,7 +85,7 @@ public class FieldProcessorTests {
 		entry.put("b", "bravo");
 		List> alpha = Arrays.asList(entry, entry);
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[].b"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[].b"), payload),
 				equalTo((Object) Arrays.asList("bravo", "bravo")));
 	}
 
@@ -98,7 +98,7 @@ public class FieldProcessorTests {
 		List>> alpha = Arrays.asList(
 				Arrays.asList(entry1, entry2), Arrays.asList(entry3));
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][]"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][]"), payload),
 				equalTo((Object) Arrays.asList(entry1, entry2, entry3)));
 	}
 
@@ -111,7 +111,7 @@ public class FieldProcessorTests {
 		List>> alpha = Arrays.asList(
 				Arrays.asList(entry1, entry2), Arrays.asList(entry3));
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].id"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload),
 				equalTo((Object) Arrays.asList("1", "2", "3")));
 	}
 
@@ -124,7 +124,7 @@ public class FieldProcessorTests {
 		List>> alpha = Arrays.asList(
 				Arrays.asList(entry1, entry2), Arrays.asList(entry3));
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].ids"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), payload),
 				equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3),
 						Arrays.asList(4))));
 	}
@@ -132,21 +132,21 @@ public class FieldProcessorTests {
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentTopLevelField() {
 		this.fieldProcessor
-				.extract(FieldPath.compile("a"), new HashMap());
+				.extract(JsonFieldPath.compile("a"), new HashMap());
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentNestedField() {
 		HashMap payload = new HashMap();
 		payload.put("a", new HashMap());
-		this.fieldProcessor.extract(FieldPath.compile("a.b"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentNestedFieldWhenParentIsNotAMap() {
 		HashMap payload = new HashMap();
 		payload.put("a", 5);
-		this.fieldProcessor.extract(FieldPath.compile("a.b"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
@@ -155,20 +155,20 @@ public class FieldProcessorTests {
 		HashMap alpha = new HashMap();
 		alpha.put("b", Arrays.asList(new HashMap()));
 		payload.put("a", alpha);
-		this.fieldProcessor.extract(FieldPath.compile("a.b.c"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b.c"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentArrayField() {
 		HashMap payload = new HashMap();
-		this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentArrayFieldAsTypeDoesNotMatch() {
 		HashMap payload = new HashMap();
 		payload.put("a", 5);
-		this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
@@ -177,14 +177,14 @@ public class FieldProcessorTests {
 		HashMap alpha = new HashMap();
 		alpha.put("b", Arrays.asList(new HashMap()));
 		payload.put("a", alpha);
-		this.fieldProcessor.extract(FieldPath.compile("a.b[].id"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b[].id"), payload);
 	}
 
 	@Test
 	public void removeTopLevelMapEntry() {
 		Map payload = new HashMap<>();
 		payload.put("a", "alpha");
-		this.fieldProcessor.remove(FieldPath.compile("a"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
@@ -194,7 +194,7 @@ public class FieldProcessorTests {
 		Map alpha = new HashMap<>();
 		payload.put("a", alpha);
 		alpha.put("b", "bravo");
-		this.fieldProcessor.remove(FieldPath.compile("a.b"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a.b"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
@@ -203,7 +203,7 @@ public class FieldProcessorTests {
 	public void removeItemsInArray() throws IOException {
 		Map payload = new ObjectMapper().readValue(
 				"{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class);
-		this.fieldProcessor.remove(FieldPath.compile("a[].b"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a[].b"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
@@ -212,7 +212,7 @@ public class FieldProcessorTests {
 	public void removeItemsInNestedArray() throws IOException {
 		Map payload = new ObjectMapper().readValue(
 				"{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class);
-		this.fieldProcessor.remove(FieldPath.compile("a[][].id"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a[][].id"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java
similarity index 79%
rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java
rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java
index 4376bea5..46868951 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java
@@ -29,66 +29,66 @@ import org.junit.rules.ExpectedException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 /**
- * Tests for {@link FieldTypeResolver}
+ * Tests for {@link JsonFieldTypeResolver}
  * 
  * @author Andy Wilkinson
  *
  */
-public class FieldTypeResolverTests {
+public class JsonFieldTypeResolverTests {
 
-	private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
+	private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver();
 
 	@Rule
 	public ExpectedException thrownException = ExpectedException.none();
 
 	@Test
 	public void arrayField() throws IOException {
-		assertFieldType(FieldType.ARRAY, "[]");
+		assertFieldType(JsonFieldType.ARRAY, "[]");
 	}
 
 	@Test
 	public void booleanField() throws IOException {
-		assertFieldType(FieldType.BOOLEAN, "true");
+		assertFieldType(JsonFieldType.BOOLEAN, "true");
 	}
 
 	@Test
 	public void objectField() throws IOException {
-		assertFieldType(FieldType.OBJECT, "{}");
+		assertFieldType(JsonFieldType.OBJECT, "{}");
 	}
 
 	@Test
 	public void nullField() throws IOException {
-		assertFieldType(FieldType.NULL, "null");
+		assertFieldType(JsonFieldType.NULL, "null");
 	}
 
 	@Test
 	public void numberField() throws IOException {
-		assertFieldType(FieldType.NUMBER, "1.2345");
+		assertFieldType(JsonFieldType.NUMBER, "1.2345");
 	}
 
 	@Test
 	public void stringField() throws IOException {
-		assertFieldType(FieldType.STRING, "\"Foo\"");
+		assertFieldType(JsonFieldType.STRING, "\"Foo\"");
 	}
 
 	@Test
 	public void nestedField() throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("a.b.c",
-				createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(FieldType.OBJECT));
+				createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(JsonFieldType.OBJECT));
 	}
 
 	@Test
 	public void multipleFieldsWithSameType() throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
 				createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")),
-				equalTo(FieldType.NUMBER));
+				equalTo(JsonFieldType.NUMBER));
 	}
 
 	@Test
 	public void multipleFieldsWithDifferentTypes() throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
 				createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")),
-				equalTo(FieldType.VARIES));
+				equalTo(JsonFieldType.VARIES));
 	}
 
 	@Test
@@ -99,7 +99,7 @@ public class FieldTypeResolverTests {
 		this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}"));
 	}
 
-	private void assertFieldType(FieldType expectedType, String jsonValue)
+	private void assertFieldType(JsonFieldType expectedType, String jsonValue)
 			throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("field",
 				createSimplePayload(jsonValue)), equalTo(expectedType));
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java
index d81ce320..fb257e54 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java
@@ -36,6 +36,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.restdocs.snippet.SnippetException;
@@ -65,8 +66,8 @@ public class RequestFieldsSnippetTests {
 						.row("a.c", "String", "two") //
 						.row("a", "Object", "three"));
 
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one"), fieldWithPath("a.c").description("two"),
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"),
+				fieldWithPath("a.c").description("two"),
 				fieldWithPath("a").description("three"))).document(
 				"map-request-with-fields",
 				result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}")));
@@ -80,9 +81,9 @@ public class RequestFieldsSnippetTests {
 						.row("[]a.c", "String", "two") //
 						.row("[]a", "Object", "three"));
 
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
-				.description("one"), fieldWithPath("[]a.c").description("two"),
-				fieldWithPath("[]a").description("three"))).document(
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"),
+				fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a")
+						.description("three"))).document(
 				"array-request-with-fields",
 				result(get("/foo").content(
 						"[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")));
@@ -94,9 +95,8 @@ public class RequestFieldsSnippetTests {
 		this.thrown
 				.expectMessage(startsWith("The following parts of the payload were not"
 						+ " documented:"));
-		new RequestFieldsSnippet(Collections. emptyList())
-				.document("undocumented-request-field",
-						result(get("/foo").content("{\"a\": 5}")));
+		new RequestFieldsSnippet(Collections. emptyList()).document(
+				"undocumented-request-field", result(get("/foo").content("{\"a\": 5}")));
 	}
 
 	@Test
@@ -105,18 +105,16 @@ public class RequestFieldsSnippetTests {
 		this.thrown
 				.expectMessage(equalTo("Fields with the following paths were not found"
 						+ " in the payload: [a.b]"));
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one"))).document("missing-request-fields",
-				result(get("/foo").content("{}")));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
+				.document("missing-request-fields", result(get("/foo").content("{}")));
 	}
 
 	@Test
 	public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException {
 		this.thrown.expect(FieldTypeRequiredException.class);
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one").optional())).document(
-				"missing-optional-request-field-with-no-type", result(get("/foo")
-						.content("{ }")));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")
+				.optional())).document("missing-optional-request-field-with-no-type",
+				result(get("/foo").content("{ }")));
 	}
 
 	@Test
@@ -128,10 +126,9 @@ public class RequestFieldsSnippetTests {
 		this.thrown
 				.expectMessage(endsWith("Fields with the following paths were not found"
 						+ " in the payload: [a.b]"));
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one"))).document(
-				"undocumented-request-field-and-missing-request-field",
-				result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
+				.document("undocumented-request-field-and-missing-request-field",
+						result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
 	}
 
 	@Test
@@ -171,10 +168,74 @@ public class RequestFieldsSnippetTests {
 				resolver));
 		request.setContent("{\"a\": \"foo\"}".getBytes());
 		MockHttpServletResponse response = new MockHttpServletResponse();
-		new RequestFieldsSnippet(attributes(key("title").value(
-				"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
-				.document("request-fields-with-custom-attributes",
-						result(request, response));
+		new RequestFieldsSnippet(attributes(key("title").value("Custom title")),
+				Arrays.asList(fieldWithPath("a").description("one"))).document(
+				"request-fields-with-custom-attributes", result(request, response));
+	}
+
+	@Test
+	public void xmlRequestFields() throws IOException {
+		this.snippet.expectRequestFields("xml-request").withContents( //
+				tableWithHeader("Path", "Type", "Description") //
+						.row("a/b", "b", "one") //
+						.row("a/c", "c", "two") //
+						.row("a", "a", "three"));
+
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
+				.type("b"), fieldWithPath("a/c").description("two").type("c"),
+				fieldWithPath("a").description("three").type("a"))).document(
+				"xml-request",
+				result(get("/foo").content("5charlie").contentType(
+						MediaType.APPLICATION_XML)));
+	}
+
+	@Test
+	public void undocumentedXmlRequestField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		new RequestFieldsSnippet(Collections. emptyList()).document(
+				"undocumented-xml-request-field",
+				result(get("/foo").content("5").contentType(
+						MediaType.APPLICATION_XML)));
+	}
+
+	@Test
+	public void xmlRequestFieldWithNoType() throws IOException {
+		this.thrown.expect(FieldTypeRequiredException.class);
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
+				.document("missing-xml-request",
+						result(get("/foo").contentType(MediaType.APPLICATION_XML)
+								.content("5")));
+	}
+
+	@Test
+	public void missingXmlRequestField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(equalTo("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
+				fieldWithPath("a").description("one"))).document(
+				"missing-xml-request-fields",
+				result(get("/foo").contentType(MediaType.APPLICATION_XML).content(
+						"")));
+	}
+
+	@Test
+	public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		this.thrown
+				.expectMessage(endsWith("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
+				.document("undocumented-xml-request-field-and-missing-xml-request-field",
+						result(get("/foo").contentType(MediaType.APPLICATION_XML)
+								.content("5")));
 	}
 
 	private FileSystemResource snippetResource(String name) {
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java
index b39802b5..a768f05b 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java
@@ -15,6 +15,8 @@
  */
 package org.springframework.restdocs.payload;
 
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.startsWith;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -26,13 +28,16 @@ import static org.springframework.restdocs.test.StubMvcResult.result;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collections;
 
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.restdocs.snippet.SnippetException;
 import org.springframework.restdocs.templates.TemplateEngine;
 import org.springframework.restdocs.templates.TemplateResourceResolver;
 import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
@@ -66,10 +71,10 @@ public class ResponseFieldsSnippetTests {
 		response.getWriter().append(
 				"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
 						+ " [{\"id\":356,\"name\": \"sample\"}]}");
-		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id")
-				.description("one"), fieldWithPath("date").description("two"),
-				fieldWithPath("assets").description("three"), fieldWithPath("assets[]")
-						.description("four"),
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"),
+				fieldWithPath("date").description("two"), fieldWithPath("assets")
+						.description("three"),
+				fieldWithPath("assets[]").description("four"),
 				fieldWithPath("assets[].id").description("five"),
 				fieldWithPath("assets[].name").description("six"))).document(
 				"map-response-with-fields", result(response));
@@ -86,10 +91,10 @@ public class ResponseFieldsSnippetTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		response.getWriter()
 				.append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]");
-		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
-				.description("one"), fieldWithPath("[]a.c").description("two"),
-				fieldWithPath("[]a").description("three"))).document(
-				"array-response-with-fields", result(response));
+		new ResponseFieldsSnippet(Arrays.asList(
+				fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c")
+						.description("two"), fieldWithPath("[]a").description("three")))
+				.document("array-response-with-fields", result(response));
 	}
 
 	@Test
@@ -100,8 +105,8 @@ public class ResponseFieldsSnippetTests {
 
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		response.getWriter().append("[\"a\", \"b\", \"c\"]");
-		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]")
-				.description("one"))).document("array-response", result(response));
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one")))
+				.document("array-response", result(response));
 	}
 
 	@Test
@@ -142,10 +147,80 @@ public class ResponseFieldsSnippetTests {
 				resolver));
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		response.getOutputStream().print("{\"a\": \"foo\"}");
-		new ResponseFieldsSnippet(attributes(key("title").value(
-				"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
-				.document("response-fields-with-custom-attributes",
-						result(request, response));
+		new ResponseFieldsSnippet(attributes(key("title").value("Custom title")),
+				Arrays.asList(fieldWithPath("a").description("one"))).document(
+				"response-fields-with-custom-attributes", result(request, response));
+	}
+
+	@Test
+	public void xmlResponseFields() throws IOException {
+		this.snippet.expectResponseFields("xml-response").withContents( //
+				tableWithHeader("Path", "Type", "Description") //
+						.row("a/b", "b", "one") //
+						.row("a/c", "c", "two") //
+						.row("a", "a", "three"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5charlie");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
+				.type("b"), fieldWithPath("a/c").description("two").type("c"),
+				fieldWithPath("a").description("three").type("a"))).document(
+				"xml-response", result(response));
+	}
+
+	@Test
+	public void undocumentedXmlResponseField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5");
+		new ResponseFieldsSnippet(Collections. emptyList()).document(
+				"undocumented-xml-response-field", result(response));
+	}
+
+	@Test
+	public void xmlResponseFieldWithNoType() throws IOException {
+		this.thrown.expect(FieldTypeRequiredException.class);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
+				.document("xml-response-no-field-type", result(response));
+	}
+
+	@Test
+	public void missingXmlResponseField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(equalTo("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
+				fieldWithPath("a").description("one"))).document(
+				"missing-xml-response-field", result(response));
+	}
+
+	@Test
+	public void undocumentedXmlResponseFieldAndMissingXmlResponseField()
+			throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		this.thrown
+				.expectMessage(endsWith("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
+				.document("undocumented-xml-request-field-and-missing-xml-request-field",
+						result(response));
 	}
 
 	private FileSystemResource snippetResource(String name) {