Ver código fonte

Add get field mappings to High Level REST API Client (#31423)

Add get field mappings to High Level REST API Client
Relates to #27205
Vladimir Dolzhenko 7 anos atrás
pai
commit
b7ef75fed6

+ 31 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java

@@ -37,6 +37,8 @@ import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
@@ -188,6 +190,35 @@ public final class IndicesClient {
             GetMappingsResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Retrieves the field mappings on an index or indices using the Get Field Mapping API.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html">
+     * Get Field Mapping API on elastic.co</a>
+     * @param getFieldMappingsRequest the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public GetFieldMappingsResponse getFieldMapping(GetFieldMappingsRequest getFieldMappingsRequest,
+                                                    RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(getFieldMappingsRequest, RequestConverters::getFieldMapping, options,
+            GetFieldMappingsResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Asynchronously retrieves the field mappings on an index on indices using the Get Field Mapping API.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html">
+     * Get Field Mapping API on elastic.co</a>
+     * @param getFieldMappingsRequest the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener the listener to be notified upon request completion
+     */
+    public void getFieldMappingAsync(GetFieldMappingsRequest getFieldMappingsRequest, RequestOptions options,
+                                     ActionListener<GetFieldMappingsResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(getFieldMappingsRequest, RequestConverters::getFieldMapping, options,
+            GetFieldMappingsResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Updates aliases using the Index Aliases API.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html">

+ 20 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

@@ -50,6 +50,7 @@ import org.elasticsearch.action.admin.indices.flush.FlushRequest;
 import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsRequest;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
@@ -230,6 +231,25 @@ final class RequestConverters {
         return request;
     }
 
+    static Request getFieldMapping(GetFieldMappingsRequest getFieldMappingsRequest) throws IOException {
+        String[] indices = getFieldMappingsRequest.indices() == null ? Strings.EMPTY_ARRAY : getFieldMappingsRequest.indices();
+        String[] types = getFieldMappingsRequest.types() == null ? Strings.EMPTY_ARRAY : getFieldMappingsRequest.types();
+        String[] fields = getFieldMappingsRequest.fields() == null ? Strings.EMPTY_ARRAY : getFieldMappingsRequest.fields();
+
+        String endpoint = new EndpointBuilder().addCommaSeparatedPathParts(indices)
+            .addPathPartAsIs("_mapping").addCommaSeparatedPathParts(types)
+            .addPathPartAsIs("field").addCommaSeparatedPathParts(fields)
+            .build();
+
+        Request request = new Request(HttpGet.METHOD_NAME, endpoint);
+
+        Params parameters = new Params(request);
+        parameters.withIndicesOptions(getFieldMappingsRequest.indicesOptions());
+        parameters.withIncludeDefaults(getFieldMappingsRequest.includeDefaults());
+        parameters.withLocal(getFieldMappingsRequest.local());
+        return request;
+    }
+
     static Request refresh(RefreshRequest refreshRequest) {
         String[] indices = refreshRequest.indices() == null ? Strings.EMPTY_ARRAY : refreshRequest.indices();
         Request request = new Request(HttpPost.METHOD_NAME, endpoint(indices, "_refresh"));

+ 38 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java

@@ -43,6 +43,8 @@ import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
@@ -74,6 +76,7 @@ import org.elasticsearch.cluster.metadata.AliasMetaData;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
 import org.elasticsearch.cluster.metadata.IndexTemplateMetaData;
 import org.elasticsearch.common.ValidationException;
+import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.ByteSizeUnit;
@@ -378,6 +381,41 @@ public class IndicesClientIT extends ESRestHighLevelClientTestCase {
         assertThat(mappings, equalTo(expected));
     }
 
+    public void testGetFieldMapping() throws IOException {
+        String indexName = "test";
+        createIndex(indexName, Settings.EMPTY);
+
+        PutMappingRequest putMappingRequest = new PutMappingRequest(indexName);
+        putMappingRequest.type("_doc");
+        XContentBuilder mappingBuilder = JsonXContent.contentBuilder();
+        mappingBuilder.startObject().startObject("properties").startObject("field");
+        mappingBuilder.field("type", "text");
+        mappingBuilder.endObject().endObject().endObject();
+        putMappingRequest.source(mappingBuilder);
+
+        PutMappingResponse putMappingResponse =
+            execute(putMappingRequest, highLevelClient().indices()::putMapping, highLevelClient().indices()::putMappingAsync);
+        assertTrue(putMappingResponse.isAcknowledged());
+
+        GetFieldMappingsRequest getFieldMappingsRequest = new GetFieldMappingsRequest()
+            .indices(indexName)
+            .types("_doc")
+            .fields("field");
+
+        GetFieldMappingsResponse getFieldMappingsResponse =
+            execute(getFieldMappingsRequest,
+                highLevelClient().indices()::getFieldMapping,
+                highLevelClient().indices()::getFieldMappingAsync);
+
+        final Map<String, GetFieldMappingsResponse.FieldMappingMetaData> fieldMappingMap =
+            getFieldMappingsResponse.mappings().get(indexName).get("_doc");
+
+        final GetFieldMappingsResponse.FieldMappingMetaData metaData =
+            new GetFieldMappingsResponse.FieldMappingMetaData("field",
+                new BytesArray("{\"field\":{\"type\":\"text\"}}"));
+        assertThat(fieldMappingMap, equalTo(Collections.singletonMap("field", metaData)));
+    }
+
     public void testDeleteIndex() throws IOException {
         {
             // Delete index if exists

+ 62 - 2
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

@@ -52,6 +52,7 @@ import org.elasticsearch.action.admin.indices.flush.FlushRequest;
 import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsRequest;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
@@ -457,6 +458,61 @@ public class RequestConvertersTests extends ESTestCase {
         assertThat(HttpGet.METHOD_NAME, equalTo(request.getMethod()));
     }
 
+    public void testGetFieldMapping() throws IOException {
+        GetFieldMappingsRequest getFieldMappingsRequest = new GetFieldMappingsRequest();
+
+        String[] indices = Strings.EMPTY_ARRAY;
+        if (randomBoolean()) {
+            indices = randomIndicesNames(0, 5);
+            getFieldMappingsRequest.indices(indices);
+        } else if (randomBoolean()) {
+            getFieldMappingsRequest.indices((String[]) null);
+        }
+
+        String type = null;
+        if (randomBoolean()) {
+            type = randomAlphaOfLengthBetween(3, 10);
+            getFieldMappingsRequest.types(type);
+        } else if (randomBoolean()) {
+            getFieldMappingsRequest.types((String[]) null);
+        }
+
+        String[] fields = null;
+        if (randomBoolean()) {
+            fields = new String[randomIntBetween(1, 5)];
+            for (int i = 0; i < fields.length; i++) {
+                fields[i] = randomAlphaOfLengthBetween(3, 10);
+            }
+            getFieldMappingsRequest.fields(fields);
+        } else if (randomBoolean()) {
+            getFieldMappingsRequest.fields((String[]) null);
+        }
+
+        Map<String, String> expectedParams = new HashMap<>();
+
+        setRandomIndicesOptions(getFieldMappingsRequest::indicesOptions, getFieldMappingsRequest::indicesOptions, expectedParams);
+        setRandomLocal(getFieldMappingsRequest::local, expectedParams);
+
+        Request request = RequestConverters.getFieldMapping(getFieldMappingsRequest);
+        StringJoiner endpoint = new StringJoiner("/", "/", "");
+        String index = String.join(",", indices);
+        if (Strings.hasLength(index)) {
+            endpoint.add(index);
+        }
+        endpoint.add("_mapping");
+        if (type != null) {
+            endpoint.add(type);
+        }
+        endpoint.add("field");
+        if (fields != null) {
+            endpoint.add(String.join(",", fields));
+        }
+        assertThat(endpoint.toString(), equalTo(request.getEndpoint()));
+
+        assertThat(expectedParams, equalTo(request.getParameters()));
+        assertThat(HttpGet.METHOD_NAME, equalTo(request.getMethod()));
+    }
+
     public void testDeleteIndex() {
         String[] indices = randomIndicesNames(0, 5);
         DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indices);
@@ -2268,16 +2324,20 @@ public class RequestConvertersTests extends ESTestCase {
         }
     }
 
-    private static void setRandomLocal(MasterNodeReadRequest<?> request, Map<String, String> expectedParams) {
+    private static void setRandomLocal(Consumer<Boolean> setter, Map<String, String> expectedParams) {
         if (randomBoolean()) {
             boolean local = randomBoolean();
-            request.local(local);
+            setter.accept(local);
             if (local) {
                 expectedParams.put("local", String.valueOf(local));
             }
         }
     }
 
+    private static void setRandomLocal(MasterNodeReadRequest<?> request, Map<String, String> expectedParams) {
+        setRandomLocal(request::local, expectedParams);
+    }
+
     private static void setRandomTimeout(Consumer<String> setter, TimeValue defaultTimeout, Map<String, String> expectedParams) {
         if (randomBoolean()) {
             String timeout = randomTimeValue();

+ 106 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java

@@ -41,6 +41,8 @@ import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
 import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
@@ -703,6 +705,110 @@ public class IndicesClientDocumentationIT extends ESRestHighLevelClientTestCase
         }
     }
 
+    public void testGetFieldMapping() throws IOException, InterruptedException {
+        RestHighLevelClient client = highLevelClient();
+
+        {
+            CreateIndexResponse createIndexResponse = client.indices().create(new CreateIndexRequest("twitter"), RequestOptions.DEFAULT);
+            assertTrue(createIndexResponse.isAcknowledged());
+            PutMappingRequest request = new PutMappingRequest("twitter");
+            request.type("tweet");
+            request.source(
+                "{\n" +
+                    "  \"properties\": {\n" +
+                    "    \"message\": {\n" +
+                    "      \"type\": \"text\"\n" +
+                    "    },\n" +
+                    "    \"timestamp\": {\n" +
+                    "      \"type\": \"date\"\n" +
+                    "    }\n" +
+                    "  }\n" +
+                    "}", // <1>
+                XContentType.JSON);
+            PutMappingResponse putMappingResponse = client.indices().putMapping(request, RequestOptions.DEFAULT);
+            assertTrue(putMappingResponse.isAcknowledged());
+        }
+
+        // tag::get-field-mapping-request
+        GetFieldMappingsRequest request = new GetFieldMappingsRequest(); // <1>
+        request.indices("twitter"); // <2>
+        request.types("tweet"); // <3>
+        request.fields("message", "timestamp"); // <4>
+        // end::get-field-mapping-request
+
+        // tag::get-field-mapping-request-indicesOptions
+        request.indicesOptions(IndicesOptions.lenientExpandOpen()); // <1>
+        // end::get-field-mapping-request-indicesOptions
+
+        // tag::get-field-mapping-request-local
+        request.local(true); // <1>
+        // end::get-field-mapping-request-local
+
+        {
+
+            // tag::get-field-mapping-execute
+            GetFieldMappingsResponse response =
+                client.indices().getFieldMapping(request, RequestOptions.DEFAULT);
+            // end::get-field-mapping-execute
+
+            // tag::get-field-mapping-response
+            final Map<String, Map<String, Map<String, GetFieldMappingsResponse.FieldMappingMetaData>>> mappings =
+                response.mappings();// <1>
+            final Map<String, GetFieldMappingsResponse.FieldMappingMetaData> typeMappings =
+                mappings.get("twitter").get("tweet"); // <2>
+            final GetFieldMappingsResponse.FieldMappingMetaData metaData =
+                typeMappings.get("message");// <3>
+
+            final String fullName = metaData.fullName();// <4>
+            final Map<String, Object> source = metaData.sourceAsMap(); // <5>
+            // end::get-field-mapping-response
+        }
+
+        {
+            // tag::get-field-mapping-execute-listener
+            ActionListener<GetFieldMappingsResponse> listener =
+                new ActionListener<GetFieldMappingsResponse>() {
+                    @Override
+                    public void onResponse(GetFieldMappingsResponse putMappingResponse) {
+                        // <1>
+                    }
+
+                    @Override
+                    public void onFailure(Exception e) {
+                        // <2>
+                    }
+                };
+            // end::get-field-mapping-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            final ActionListener<GetFieldMappingsResponse> latchListener = new LatchedActionListener<>(listener, latch);
+            listener = ActionListener.wrap(r -> {
+                final Map<String, Map<String, Map<String, GetFieldMappingsResponse.FieldMappingMetaData>>> mappings =
+                    r.mappings();
+                final Map<String, GetFieldMappingsResponse.FieldMappingMetaData> typeMappings =
+                    mappings.get("twitter").get("tweet");
+                final GetFieldMappingsResponse.FieldMappingMetaData metaData1 = typeMappings.get("message");
+
+                final String fullName = metaData1.fullName();
+                final Map<String, Object> source = metaData1.sourceAsMap();
+                latchListener.onResponse(r);
+            }, e -> {
+                latchListener.onFailure(e);
+                fail("should not fail");
+            });
+
+            // tag::get-field-mapping-execute-async
+            client.indices().getFieldMappingAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::get-field-mapping-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+
+
+    }
+
+
     public void testOpenIndex() throws Exception {
         RestHighLevelClient client = highLevelClient();
 

+ 86 - 0
docs/java-rest/high-level/indices/get_field_mappings.asciidoc

@@ -0,0 +1,86 @@
+[[java-rest-high-get-field-mappings]]
+=== Get Field Mappings API
+
+[[java-rest-high-get-field-mappings-request]]
+==== Get Field Mappings Request
+
+A `GetFieldMappingsRequest` can have an optional list of indices, optional list of types and the list of fields:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-field-mapping-request]
+--------------------------------------------------
+<1> An empty request
+<2> Setting the indices to fetch mapping for
+<3> The types to be returned
+<4> The fields to be returned
+
+==== Optional arguments
+The following arguments can also optionally be provided:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-field-mapping-request-indicesOptions]
+--------------------------------------------------
+<1> Setting `IndicesOptions` controls how unavailable indices are resolved and
+how wildcard expressions are expanded
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-field-mapping-request-local]
+--------------------------------------------------
+<1> The `local` flag (defaults to `false`) controls whether the aliases need
+to be looked up in the local cluster state or in the cluster state held by
+the elected master node
+
+[[java-rest-high-get-field-mappings-sync]]
+==== Synchronous Execution
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-field-mapping-execute]
+--------------------------------------------------
+
+[[java-rest-high-get-field-mapping-async]]
+==== Asynchronous Execution
+
+The asynchronous execution of a get mappings request requires both the
+`GetFieldMappingsRequest` instance and an `ActionListener` instance to be passed to
+the asynchronous method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-field-mapping-execute-async]
+--------------------------------------------------
+<1> The `GetFieldMappingsRequest` to execute and the `ActionListener` to use when the execution completes
+
+The asynchronous method does not block and returns immediately. Once it is
+completed the `ActionListener` is called back using the `onResponse` method if
+the execution successfully completed or using the `onFailure` method if it
+failed.
+
+A typical listener for `GetMappingsResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-field-mapping-execute-listener]
+--------------------------------------------------
+<1> Called when the execution is successfully completed. The response is provided as an argument
+<2> Called in case of failure. The raised exception is provided as an argument
+
+[[java-rest-high-get-field-mapping-response]]
+==== Get Field Mappings Response
+
+The returned `GetFieldMappingsResponse` allows to retrieve information about the
+executed operation as follows:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-field-mapping-response]
+--------------------------------------------------
+<1> Returning all requested indices fields' mappings
+<2> Retrieving the mappings for a particular index and type
+<3> Getting the mappings metadata for the `message` field
+<4> Getting the full name of the field
+<5> Getting the mapping source of the field
+

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -77,6 +77,7 @@ Index Management::
 
 Mapping Management::
 * <<java-rest-high-put-mapping>>
+* <<java-rest-high-get-field-mappings>>
 
 Alias Management::
 * <<java-rest-high-update-aliases>>
@@ -98,6 +99,7 @@ include::indices/force_merge.asciidoc[]
 include::indices/rollover.asciidoc[]
 include::indices/put_mapping.asciidoc[]
 include::indices/get_mappings.asciidoc[]
+include::indices/get_field_mappings.asciidoc[]
 include::indices/update_aliases.asciidoc[]
 include::indices/exists_alias.asciidoc[]
 include::indices/get_alias.asciidoc[]

+ 120 - 3
server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponse.java

@@ -20,13 +20,17 @@
 package org.elasticsearch.action.admin.indices.mapping.get;
 
 import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.ToXContentFragment;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.mapper.Mapper;
 
@@ -34,13 +38,45 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.unmodifiableMap;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
 
 /** Response object for {@link GetFieldMappingsRequest} API */
 public class GetFieldMappingsResponse extends ActionResponse implements ToXContentFragment {
 
+    private static final ParseField MAPPINGS = new ParseField("mappings");
+
+    private static final ObjectParser<Map<String, Map<String, FieldMappingMetaData>>, String> PARSER =
+        new ObjectParser<>(MAPPINGS.getPreferredName(), true, HashMap::new);
+
+    static {
+        PARSER.declareField((p, typeMappings, index) -> {
+            p.nextToken();
+            while (p.currentToken() == XContentParser.Token.FIELD_NAME) {
+                final String typeName = p.currentName();
+
+                if (p.nextToken() == XContentParser.Token.START_OBJECT) {
+                    final Map<String, FieldMappingMetaData> typeMapping = new HashMap<>();
+                    typeMappings.put(typeName, typeMapping);
+
+                    while (p.nextToken() == XContentParser.Token.FIELD_NAME) {
+                        final String fieldName = p.currentName();
+                        final FieldMappingMetaData fieldMappingMetaData = FieldMappingMetaData.fromXContent(p);
+                        typeMapping.put(fieldName, fieldMappingMetaData);
+                    }
+                } else {
+                    p.skipChildren();
+                }
+                p.nextToken();
+            }
+        }, MAPPINGS, ObjectParser.ValueType.OBJECT);
+    }
+
     private Map<String, Map<String, Map<String, FieldMappingMetaData>>> mappings = emptyMap();
 
     GetFieldMappingsResponse(Map<String, Map<String, Map<String, FieldMappingMetaData>>> mappings) {
@@ -77,7 +113,7 @@ public class GetFieldMappingsResponse extends ActionResponse implements ToXConte
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         for (Map.Entry<String, Map<String, Map<String, FieldMappingMetaData>>> indexEntry : mappings.entrySet()) {
             builder.startObject(indexEntry.getKey());
-            builder.startObject("mappings");
+            builder.startObject(MAPPINGS.getPreferredName());
             for (Map.Entry<String, Map<String, FieldMappingMetaData>> typeEntry : indexEntry.getValue().entrySet()) {
                 builder.startObject(typeEntry.getKey());
                 for (Map.Entry<String, FieldMappingMetaData> fieldEntry : typeEntry.getValue().entrySet()) {
@@ -93,9 +129,46 @@ public class GetFieldMappingsResponse extends ActionResponse implements ToXConte
         return builder;
     }
 
+    public static GetFieldMappingsResponse fromXContent(XContentParser parser) throws IOException {
+        ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
+
+        final Map<String, Map<String, Map<String, FieldMappingMetaData>>> mappings = new HashMap<>();
+        if (parser.nextToken() == XContentParser.Token.FIELD_NAME) {
+            while (parser.currentToken() == XContentParser.Token.FIELD_NAME) {
+                final String index = parser.currentName();
+
+                final Map<String, Map<String, FieldMappingMetaData>> typeMappings = PARSER.parse(parser, index);
+                mappings.put(index, typeMappings);
+
+                parser.nextToken();
+            }
+        }
+
+        return new GetFieldMappingsResponse(mappings);
+    }
+
     public static class FieldMappingMetaData implements ToXContentFragment {
         public static final FieldMappingMetaData NULL = new FieldMappingMetaData("", BytesArray.EMPTY);
 
+        private static final ParseField FULL_NAME = new ParseField("full_name");
+        private static final ParseField MAPPING = new ParseField("mapping");
+
+        private static final ConstructingObjectParser<FieldMappingMetaData, String> PARSER =
+            new ConstructingObjectParser<>("field_mapping_meta_data", true,
+                a -> new FieldMappingMetaData((String)a[0], (BytesReference)a[1])
+            );
+
+        static {
+            PARSER.declareField(optionalConstructorArg(),
+                (p, c) -> p.text(), FULL_NAME, ObjectParser.ValueType.STRING);
+            PARSER.declareField(optionalConstructorArg(),
+                (p, c) -> {
+                    final XContentBuilder jsonBuilder = jsonBuilder().copyCurrentStructure(p);
+                    final BytesReference bytes = BytesReference.bytes(jsonBuilder);
+                    return bytes;
+                }, MAPPING, ObjectParser.ValueType.OBJECT);
+        }
+
         private String fullName;
         private BytesReference source;
 
@@ -122,18 +195,41 @@ public class GetFieldMappingsResponse extends ActionResponse implements ToXConte
             return source;
         }
 
+        public static FieldMappingMetaData fromXContent(XContentParser parser) throws IOException {
+            return PARSER.parse(parser, null);
+        }
+
         @Override
         public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-            builder.field("full_name", fullName);
+            builder.field(FULL_NAME.getPreferredName(), fullName);
             if (params.paramAsBoolean("pretty", false)) {
                 builder.field("mapping", sourceAsMap());
             } else {
                 try (InputStream stream = source.streamInput()) {
-                    builder.rawField("mapping", stream, XContentType.JSON);
+                    builder.rawField(MAPPING.getPreferredName(), stream, XContentType.JSON);
                 }
             }
             return builder;
         }
+
+        @Override
+        public String toString() {
+            return "FieldMappingMetaData{fullName='" + fullName + '\'' + ", source=" + source + '}';
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof FieldMappingMetaData)) return false;
+            FieldMappingMetaData that = (FieldMappingMetaData) o;
+            return Objects.equals(fullName, that.fullName) &&
+                Objects.equals(source, that.source);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(fullName, source);
+        }
     }
 
     @Override
@@ -178,4 +274,25 @@ public class GetFieldMappingsResponse extends ActionResponse implements ToXConte
             }
         }
     }
+
+    @Override
+    public String toString() {
+        return "GetFieldMappingsResponse{" +
+            "mappings=" + mappings +
+            '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof GetFieldMappingsResponse)) return false;
+        GetFieldMappingsResponse that = (GetFieldMappingsResponse) o;
+        return Objects.equals(mappings, that.mappings);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mappings);
+    }
+
 }

+ 97 - 3
server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponseTests.java

@@ -23,16 +23,22 @@ import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsRespon
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.io.stream.StreamInput;
-import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.AbstractStreamableXContentTestCase;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Predicate;
 
-public class GetFieldMappingsResponseTests extends ESTestCase {
+import static org.hamcrest.CoreMatchers.equalTo;
 
-    public void testSerialization() throws IOException {
+public class GetFieldMappingsResponseTests extends AbstractStreamableXContentTestCase<GetFieldMappingsResponse> {
+
+    public void testManualSerialization() throws IOException {
         Map<String, Map<String, Map<String, FieldMappingMetaData>>> mappings = new HashMap<>();
         FieldMappingMetaData fieldMappingMetaData = new FieldMappingMetaData("my field", new BytesArray("{}"));
         mappings.put("index", Collections.singletonMap("type", Collections.singletonMap("field", fieldMappingMetaData)));
@@ -49,4 +55,92 @@ public class GetFieldMappingsResponseTests extends ESTestCase {
             }
         }
     }
+
+    public void testManualJunkedJson() throws Exception {
+        // in fact random fields could be evaluated as proper mapping, while proper junk in this case is arrays and values
+        final String json =
+            "{\"index1\":{\"mappings\":"
+                + "{\"doctype0\":{\"field1\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}},"
+                    + "\"field0\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}}},"
+                // junk here
+                + "\"junk1\": [\"field1\", {\"field2\":{}}],"
+                + "\"junk2\": [{\"field3\":{}}],"
+                + "\"junk3\": 42,"
+                + "\"junk4\": \"Q\","
+                + "\"doctype1\":{\"field1\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}},"
+                    + "\"field0\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}}}}},"
+            + "\"index0\":{\"mappings\":"
+                + "{\"doctype0\":{\"field1\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}},"
+                + "\"field0\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}}},"
+                + "\"doctype1\":{\"field1\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}},"
+                + "\"field0\":{\"full_name\":\"my field\",\"mapping\":{\"type\":\"keyword\"}}}}}}";
+
+        final XContentParser parser = XContentType.JSON.xContent().createParser(xContentRegistry(),
+            LoggingDeprecationHandler.INSTANCE, json.getBytes("UTF-8"));
+
+        final GetFieldMappingsResponse response = GetFieldMappingsResponse.fromXContent(parser);
+
+        FieldMappingMetaData fieldMappingMetaData =
+            new FieldMappingMetaData("my field", new BytesArray("{\"type\":\"keyword\"}"));
+        Map<String, FieldMappingMetaData> fieldMapping = new HashMap<>();
+        fieldMapping.put("field0", fieldMappingMetaData);
+        fieldMapping.put("field1", fieldMappingMetaData);
+
+        Map<String, Map<String, FieldMappingMetaData>> typeMapping = new HashMap<>();
+        typeMapping.put("doctype0", fieldMapping);
+        typeMapping.put("doctype1", fieldMapping);
+
+        Map<String, Map<String, Map<String, FieldMappingMetaData>>> mappings = new HashMap<>();
+        mappings.put("index0", typeMapping);
+        mappings.put("index1", typeMapping);
+
+        final Map<String, Map<String, Map<String, FieldMappingMetaData>>> responseMappings = response.mappings();
+        assertThat(responseMappings, equalTo(mappings));
+    }
+
+    @Override
+    protected GetFieldMappingsResponse doParseInstance(XContentParser parser) throws IOException {
+        return GetFieldMappingsResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected GetFieldMappingsResponse createBlankInstance() {
+        return new GetFieldMappingsResponse();
+    }
+
+    @Override
+    protected GetFieldMappingsResponse createTestInstance() {
+        return new GetFieldMappingsResponse(randomMapping());
+    }
+
+    @Override
+    protected Predicate<String> getRandomFieldsExcludeFilter() {
+        // allow random fields at the level of `index` and `index.mappings.doctype.field`
+        // otherwise random field could be evaluated as index name or type name
+        return s -> false == (s.matches("(?<index>[^.]+)")
+            || s.matches("(?<index>[^.]+)\\.mappings\\.(?<doctype>[^.]+)\\.(?<field>[^.]+)"));
+    }
+
+    private Map<String, Map<String, Map<String, FieldMappingMetaData>>> randomMapping() {
+        Map<String, Map<String, Map<String, FieldMappingMetaData>>> mappings = new HashMap<>();
+
+        int indices = randomInt(10);
+        for(int i = 0; i < indices; i++) {
+            final Map<String, Map<String, FieldMappingMetaData>> doctypesMappings = new HashMap<>();
+            int doctypes = randomInt(10);
+            for(int j = 0; j < doctypes; j++) {
+                Map<String, FieldMappingMetaData> fieldMappings = new HashMap<>();
+                int fields = randomInt(10);
+                for(int k = 0; k < fields; k++) {
+                    final String mapping = randomBoolean() ? "{\"type\":\"string\"}" : "{\"type\":\"keyword\"}";
+                    FieldMappingMetaData metaData =
+                        new FieldMappingMetaData("my field", new BytesArray(mapping));
+                    fieldMappings.put("field" + k, metaData);
+                }
+                doctypesMappings.put("doctype" + j, fieldMappings);
+            }
+            mappings.put("index" + i, doctypesMappings);
+        }
+        return mappings;
+    }
 }