Browse Source

[Rest API Compatibility] Typed endpoints for Put and Get Mapping and get field mappings (#71721)

Implements a V7 compatible typed endpoints for REST put and get mapping endpoints. Also for Get Field Mappings endpoints.
retrofits the REST layer change removed in #41676

relates main meta issue #51816
relates types removal issue #54160
Przemyslaw Gomulka 4 years ago
parent
commit
668a72dec5

+ 97 - 42
rest-api-spec/build.gradle

@@ -144,60 +144,29 @@ tasks.named("yamlRestCompatTest").configure {
     'get_source/86_source_missing_with_types/Missing document source with catch',
     'get_source/86_source_missing_with_types/Missing document source with ignore',
     'indices.create/10_basic/Create index without soft deletes',
-    // 5 below await retrofitting Removes typed URLs from mapping APIs #41676
-    'indices.create/11_basic_with_types/Create index with mappings',
-    'indices.create/20_mix_typeless_typeful/Create a typed index while there is a typeless template',
-    'indices.create/20_mix_typeless_typeful/Create a typeless index while there is a typed template',
+    // type information about the type is removed and not passed down. The logic to check for this is also removed.
     'indices.create/20_mix_typeless_typeful/Implicitly create a typed index while there is a typeless template',
     'indices.create/20_mix_typeless_typeful/Implicitly create a typeless index while there is a typed template',
     //
     'indices.flush/10_basic/Index synced flush rest test',
     'indices.forcemerge/10_basic/Check deprecation warning when incompatible only_expunge_deletes and max_num_segments values are both set',
-    'indices.get_field_mapping/10_basic/Get field mapping with local is deprecated',// awaits #41676
-    'indices.get_field_mapping/11_basic_with_types/Get field mapping by index only',// awaits #41676
-    'indices.get_field_mapping/11_basic_with_types/Get field mapping by type & field',// awaits #41676
-    'indices.get_field_mapping/11_basic_with_types/Get field mapping by type & field, with another field that doesn\'t exist',// awaits #41676
-    'indices.get_field_mapping/11_basic_with_types/Get field mapping should work without index specifying type and fields',// awaits #41676
-    'indices.get_field_mapping/11_basic_with_types/Get field mapping with include_defaults',// awaits #41676
-    'indices.get_field_mapping/11_basic_with_types/Get field mapping with no index and type',// awaits #41676
-    'indices.get_field_mapping/21_missing_field_with_types/Return empty object if field doesn\'t exist, but type and index do',// awaits #41676
-    'indices.get_field_mapping/30_missing_type/Raise 404 when type doesn\'t exist',// awaits #41676
-    'indices.get_field_mapping/51_field_wildcards_with_types/Get field mapping should work using \'*\' for indices and types',// awaits #41676
-    'indices.get_field_mapping/51_field_wildcards_with_types/Get field mapping should work using \'_all\' for indices and types',// awaits #41676
-    'indices.get_field_mapping/51_field_wildcards_with_types/Get field mapping should work using comma_separated values for indices and types',// awaits #41676
-    'indices.get_field_mapping/51_field_wildcards_with_types/Get field mapping with * for fields',// awaits #41676
-    'indices.get_field_mapping/51_field_wildcards_with_types/Get field mapping with *t1 for fields',// awaits #41676
-    'indices.get_field_mapping/51_field_wildcards_with_types/Get field mapping with t* for fields',// awaits #41676
-    'indices.get_field_mapping/51_field_wildcards_with_types/Get field mapping with wildcarded relative names',// awaits #41676
-    'indices.get_mapping/11_basic_with_types/Get /*/_mapping/{type}',
-    'indices.get_mapping/11_basic_with_types/Get /_all/_mapping/{type}',
-    'indices.get_mapping/11_basic_with_types/Get /_mapping',
-    'indices.get_mapping/11_basic_with_types/Get /_mapping/{type}',
-    'indices.get_mapping/11_basic_with_types/Get /index*/_mapping/{type}',
-    'indices.get_mapping/11_basic_with_types/Get /index,index/_mapping/{type}',
-    'indices.get_mapping/11_basic_with_types/Get /{index}/_mapping',
-    'indices.get_mapping/11_basic_with_types/Get /{index}/_mapping/*',
-    'indices.get_mapping/11_basic_with_types/Get /{index}/_mapping/_all',
-    'indices.get_mapping/11_basic_with_types/Get /{index}/_mapping/{type*}',
-    'indices.get_mapping/11_basic_with_types/Get /{index}/_mapping/{type}',
+    // This test returns test_index.mappings:{} when {} was expected. difference between 20_missing_field and 21_missing_field_with_types?
+    'indices.get_field_mapping/21_missing_field_with_types/Return empty object if field doesn\'t exist, but type and index do',
+    // The information about the type is not present in the index. hence it cannot know if the type exist or not.
+    'indices.get_field_mapping/30_missing_type/Raise 404 when type doesn\'t exist',
+    // The information about the type is not present in the index. hence it cannot know if the type exist or not.
     'indices.get_mapping/20_missing_type/Existent and non-existent type returns 404 and the existing type',
     'indices.get_mapping/20_missing_type/Existent and non-existent types returns 404 and the existing type',
     'indices.get_mapping/20_missing_type/No type matching pattern returns 404',
     'indices.get_mapping/20_missing_type/Non-existent type returns 404',
     'indices.get_mapping/20_missing_type/Type missing when no types exist',
+    //
     'indices.open/10_basic/?wait_for_active_shards default is deprecated',
     'indices.open/10_basic/?wait_for_active_shards=index-setting',
-    'indices.put_mapping/10_basic/Put mappings with explicit _doc type',
-    'indices.put_mapping/11_basic_with_types/Test Create and update mapping',
+
+    // The information about the type is not present in the index. hence it cannot know if the type was already used or not
     'indices.put_mapping/20_mix_typeless_typeful/PUT mapping with _doc on an index that has types',
     'indices.put_mapping/20_mix_typeless_typeful/PUT mapping with typeless API on an index that has types',
-    'indices.put_mapping/all_path_options_with_types/post a mapping with default analyzer twice',
-    'indices.put_mapping/all_path_options_with_types/put mapping in * index',
-    'indices.put_mapping/all_path_options_with_types/put mapping in _all index',
-    'indices.put_mapping/all_path_options_with_types/put mapping in list of indices',
-    'indices.put_mapping/all_path_options_with_types/put mapping in prefix* index',
-    'indices.put_mapping/all_path_options_with_types/put mapping with blank index',
-    'indices.put_mapping/all_path_options_with_types/put one mapping per index',
     // there is a small distinction between empty mappings and no mappings at all. The code to implement this test was refactored #54003
     // not fixing this in #70966
     'indices.put_template/11_basic_with_types/Put template with empty mappings',
@@ -297,8 +266,94 @@ tasks.named("transformV7RestTests").configure({ task ->
   task.replaceValueInMatch("nodes.\$node_id.roles.9", "remote_cluster_client", "node_info role test")
   task.removeMatch("nodes.\$node_id.roles.10", "node_info role test")
   task.replaceIsTrue("test_index.mappings.type_1", "test_index.mappings._doc")
-  task.replaceIsFalse("test_index.mappings.type_1", "test_index.mappings._doc")
-  task.replaceIsFalse("test-1.mappings.my_type", "test-1.mappings._doc")
+  //override for indices.get and indices.create
+  //task.replaceIsFalse("test_index.mappings.type_1", "test_index.mappings._doc")
+  //overrides for indices.create/20_mix_typeless_typeful
+  task.replaceIsFalse("test-1.mappings._doc","false",  "Create a typed index while there is a typeless template")
+  task.replaceIsFalse("test-1.mappings._doc","false", "Create a typeless index while there is a typed template")
+
+  task.replaceIsTrue("test-1.mappings.my_type", "test-1.mappings._doc")
+  task.replaceIsTrue("test-1.mappings.my_type.properties.foo", "test-1.mappings._doc.properties.foo")
+  task.replaceIsTrue("test-1.mappings.my_type.properties.bar", "test-1.mappings._doc.properties.bar")
+
+  // overrides for indices.get_field_mapping
+  task.replaceKeyInLength("test_index.mappings.test_type.text.mapping.text.type",
+    "test_index.mappings._doc.text.mapping.text.type"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.text.mapping.text.analyzer",
+    "test_index.mappings._doc.text.mapping.text.analyzer"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.t1.full_name",
+    "test_index.mappings._doc.t1.full_name"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.t2.full_name",
+    "test_index.mappings._doc.t2.full_name"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.obj\\.t1.full_name",
+    "test_index.mappings._doc.obj\\.t1.full_name"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.obj\\.i_t1.full_name",
+    "test_index.mappings._doc.obj\\.i_t1.full_name"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.obj\\.i_t3.full_name",
+    "test_index.mappings._doc.obj\\.i_t3.full_name"
+  )
+  task.replaceKeyInLength("test_index.mappings.test_type",
+    "test_index.mappings._doc"
+  )
+  task.replaceKeyInMatch("test_index_2.mappings.test_type_2.t1.full_name",
+    "test_index.mappings._doc.t1.full_name"
+  )
+  task.replaceKeyInMatch("test_index_2.mappings.test_type_2.t2.full_name",
+    "test_index.mappings._doc.t2.full_name"
+  )
+  task.replaceKeyInLength("test_index_2.mappings.test_type_2",
+    "test_index.mappings._doc"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.text.mapping.text.type",
+    "test_index.mappings._doc.text.mapping.text.type"
+  )
+  // overrides for indices.put_mapping/11_basic_with_types
+  task.replaceKeyInMatch("test_index.mappings.test_type.properties.text1.type",
+    "test_index.mappings._doc.properties.text1.type"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.properties.text1.analyzer",
+    "test_index.mappings._doc.properties.text1.analyzer"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.properties.text2.type",
+    "test_index.mappings._doc.properties.text2.type"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.properties.text2.analyzer",
+    "test_index.mappings._doc.properties.text2.analyzer"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.properties.subfield.properties.text3.type",
+    "test_index.mappings._doc.properties.subfield.properties.text3.type"
+  )
+  task.replaceKeyInMatch("test_index.mappings.test_type.properties.text1.fields.text_raw.type",
+    "test_index.mappings._doc.properties.text1.fields.text_raw.type"
+  )
+  // overrides for indices.put_mapping/all_path_options_with_types
+  task.replaceKeyInMatch("test_index1.mappings.test_type.properties.text.type",
+    "test_index1.mappings._doc.properties.text.type"
+  )
+  task.replaceKeyInMatch("test_index1.mappings.test_type.properties.text.analyzer",
+    "test_index1.mappings._doc.properties.text.analyzer"
+  )
+  task.replaceKeyInMatch("test_index2.mappings.test_type.properties.text.type",
+    "test_index2.mappings._doc.properties.text.type"
+  )
+  task.replaceKeyInMatch("test_index2.mappings.test_type.properties.text.analyzer",
+    "test_index2.mappings._doc.properties.text.analyzer"
+  )
+  task.replaceKeyInMatch("foo.mappings.test_type.properties.text.type",
+    "foo.mappings._doc.properties.text.type"
+  )
+  task.replaceKeyInMatch("foo.mappings.test_type.properties.text.analyzer",
+    "foo.mappings._doc.properties.text.analyzer"
+  )
+  // overrides for indices.get_mapping
+  task.replaceIsTrue("test_1.mappings.doc", "test_1.mappings._doc")
+  task.replaceIsTrue("test_2.mappings.doc", "test_2.mappings._doc")
 })
 
 tasks.register('enforceYamlTestConvention').configure {

+ 15 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java

@@ -13,6 +13,7 @@ import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.cluster.metadata.MappingMetadata;
 import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.RestApiVersion;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.ImmutableOpenMap;
 import org.elasticsearch.common.io.stream.StreamInput;
@@ -23,6 +24,9 @@ import org.elasticsearch.index.mapper.MapperService;
 
 import java.io.IOException;
 
+import static org.elasticsearch.rest.BaseRestHandler.DEFAULT_INCLUDE_TYPE_NAME_POLICY;
+import static org.elasticsearch.rest.BaseRestHandler.INCLUDE_TYPE_NAME_PARAMETER;
+
 public class GetMappingsResponse extends ActionResponse implements ToXContentFragment {
 
     private static final ParseField MAPPINGS = new ParseField("mappings");
@@ -65,7 +69,17 @@ public class GetMappingsResponse extends ActionResponse implements ToXContentFra
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         for (final ObjectObjectCursor<String, MappingMetadata> indexEntry : getMappings()) {
             builder.startObject(indexEntry.key);
-            if (indexEntry.value != null) {
+            boolean includeTypeName = params.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER,
+                DEFAULT_INCLUDE_TYPE_NAME_POLICY);
+            if (builder.getRestApiVersion() == RestApiVersion.V_7 && includeTypeName && indexEntry.value != null) {
+                builder.startObject(MAPPINGS.getPreferredName());
+
+                if (indexEntry.value != MappingMetadata.EMPTY_MAPPINGS) {
+                    builder.field(MapperService.SINGLE_MAPPING_NAME, indexEntry.value.sourceAsMap());
+                }
+                builder.endObject();
+
+            } else if (indexEntry.value != null) {
                 builder.field(MAPPINGS.getPreferredName(), indexEntry.value.sourceAsMap());
             } else {
                 builder.startObject(MAPPINGS.getPreferredName()).endObject();

+ 27 - 6
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetFieldMappingAction.java

@@ -38,14 +38,21 @@ public class RestGetFieldMappingAction extends BaseRestHandler {
 
     private static final Logger logger = LogManager.getLogger(RestGetFieldMappingAction.class);
     private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(logger.getName());
-    public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Using include_type_name in get " +
+    public static final String INCLUDE_TYPE_DEPRECATION_MESSAGE = "[types removal] Using include_type_name in get " +
         "field mapping requests is deprecated. The parameter will be removed in the next major version.";
-
+    public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Specifying types in get field mapping request is deprecated. " +
+        "Use typeless api instead";
     @Override
     public List<Route> routes() {
         return List.of(
             new Route(GET, "/_mapping/field/{fields}"),
-            new Route(GET, "/{index}/_mapping/field/{fields}"));
+            new Route(GET, "/{index}/_mapping/field/{fields}"),
+            Route.builder(GET, "/_mapping/{type}/field/{fields}")
+                .deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(GET, "/{index}/{type}/_mapping/field/{fields}")
+                .deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(GET, "/{index}/_mapping/{type}/field/{fields}")
+                .deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build());
     }
 
     @Override
@@ -58,9 +65,23 @@ public class RestGetFieldMappingAction extends BaseRestHandler {
         final String[] indices = Strings.splitStringByCommaToArray(request.param("index"));
         final String[] fields = Strings.splitStringByCommaToArray(request.param("fields"));
 
-        if (request.getRestApiVersion() == RestApiVersion.V_7 && request.hasParam(INCLUDE_TYPE_NAME_PARAMETER)) {
-            request.param(INCLUDE_TYPE_NAME_PARAMETER);
-            deprecationLogger.compatibleApiWarning("get_field_mapping_with_types", TYPES_DEPRECATION_MESSAGE);
+        if (request.getRestApiVersion() == RestApiVersion.V_7) {
+            if (request.hasParam(INCLUDE_TYPE_NAME_PARAMETER)) {
+                deprecationLogger.compatibleApiWarning("get_field_mapping_with_types", INCLUDE_TYPE_DEPRECATION_MESSAGE);
+            }
+            boolean includeTypeName = request.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY);
+            final String[] types = request.paramAsStringArrayOrEmptyIfAll("type");
+            if (includeTypeName == false && types.length > 0) {
+                throw new IllegalArgumentException("Types cannot be specified unless include_type_name" + " is set to true.");
+            }
+
+            if (request.hasParam("local")) {
+                request.param("local");
+                deprecationLogger.compatibleApiWarning(
+                    "get_field_mapping_local",
+                    "Use [local] in get field mapping requests is deprecated. " + "The parameter will be removed in the next major version"
+                );
+            }
         }
 
 

+ 24 - 6
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java

@@ -31,11 +31,14 @@ import java.util.List;
 import java.util.function.LongSupplier;
 
 import static org.elasticsearch.rest.RestRequest.Method.GET;
+import static org.elasticsearch.rest.RestRequest.Method.HEAD;
 
 public class RestGetMappingAction extends BaseRestHandler {
     private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestGetMappingAction.class);
-    public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Using include_type_name in get"
+    public static final String INCLUDE_TYPE_DEPRECATION_MSG = "[types removal] Using include_type_name in get"
         + " mapping requests is deprecated. The parameter will be removed in the next major version.";
+    public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Specifying types in get mapping request is deprecated. " +
+        "Use typeless api instead";
 
     private final ThreadPool threadPool;
 
@@ -48,8 +51,13 @@ public class RestGetMappingAction extends BaseRestHandler {
         return List.of(
             new Route(GET, "/_mapping"),
             new Route(GET, "/_mappings"),
+            Route.builder(GET, "/{index}/{type}/_mapping").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
             new Route(GET, "/{index}/_mapping"),
-            new Route(GET, "/{index}/_mappings"));
+            new Route(GET, "/{index}/_mappings"),
+            Route.builder(GET, "/{index}/_mappings/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(GET, "/{index}/_mapping/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(HEAD, "/{index}/_mapping/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(GET, "/_mapping/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build());
     }
 
     @Override
@@ -59,11 +67,21 @@ public class RestGetMappingAction extends BaseRestHandler {
 
     @Override
     public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
-        if (request.getRestApiVersion() == RestApiVersion.V_7 && request.hasParam(INCLUDE_TYPE_NAME_PARAMETER)) {
-            request.param(INCLUDE_TYPE_NAME_PARAMETER);
-            deprecationLogger.compatibleApiWarning("get_mapping_with_types", TYPES_DEPRECATION_MESSAGE);
+        if (request.getRestApiVersion() == RestApiVersion.V_7) {
+            if (request.hasParam(INCLUDE_TYPE_NAME_PARAMETER)) {
+                request.param(INCLUDE_TYPE_NAME_PARAMETER);
+                deprecationLogger.compatibleApiWarning("get_mapping_with_types", INCLUDE_TYPE_DEPRECATION_MSG);
+            }
+            final String[] types = request.paramAsStringArrayOrEmptyIfAll("type");
+            if (request.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY) == false && types.length > 0) {
+                throw new IllegalArgumentException("Types cannot be provided in get mapping requests, unless" +
+                    " include_type_name is set to true.");
+            }
+            if (request.method().equals(HEAD)) {
+                deprecationLogger.compatibleApiWarning("get_mapping_types_removal",
+                    "Type exists requests are deprecated, as types have been deprecated.");
+            }
         }
-
         final String[] indices = Strings.splitStringByCommaToArray(request.param("index"));
 
         final GetMappingsRequest getMappingsRequest = new GetMappingsRequest();

+ 53 - 4
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestPutMappingAction.java

@@ -11,7 +11,9 @@ package org.elasticsearch.rest.action.admin.indices;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.RestApiVersion;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.rest.BaseRestHandler;
@@ -23,10 +25,14 @@ import java.util.List;
 import java.util.Map;
 
 import static org.elasticsearch.client.Requests.putMappingRequest;
+import static org.elasticsearch.index.mapper.MapperService.isMappingSourceTyped;
 import static org.elasticsearch.rest.RestRequest.Method.POST;
 import static org.elasticsearch.rest.RestRequest.Method.PUT;
 
 public class RestPutMappingAction extends BaseRestHandler {
+    public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Specifying types in put mapping request is deprecated. " +
+        "Use typeless api instead";
+    private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestPutMappingAction.class);
 
     @Override
     public List<Route> routes() {
@@ -34,7 +40,19 @@ public class RestPutMappingAction extends BaseRestHandler {
             new Route(POST, "/{index}/_mapping/"),
             new Route(PUT, "/{index}/_mapping/"),
             new Route(POST, "/{index}/_mappings/"),
-            new Route(PUT, "/{index}/_mappings/"));
+            new Route(PUT, "/{index}/_mappings/"),
+            Route.builder(POST, "/{index}/{type}/_mapping").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(PUT, "/{index}/{type}/_mapping").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(POST, "/{index}/_mapping/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(PUT, "/{index}/_mapping/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(POST, "/_mapping/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(PUT, "/_mapping/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(POST, "/{index}/{type}/_mappings").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(PUT, "/{index}/{type}/_mappings").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(POST, "/{index}/_mappings/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(PUT, "/{index}/_mappings/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(POST, "/_mappings/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build(),
+            Route.builder(PUT, "/_mappings/{type}").deprecated(TYPES_DEPRECATION_MESSAGE, RestApiVersion.V_7).build());
     }
 
     @Override
@@ -48,15 +66,46 @@ public class RestPutMappingAction extends BaseRestHandler {
 
         Map<String, Object> sourceAsMap = XContentHelper.convertToMap(request.requiredContent(), false,
             request.getXContentType()).v2();
-        if (MapperService.isMappingSourceTyped(MapperService.SINGLE_MAPPING_NAME, sourceAsMap)) {
-            throw new IllegalArgumentException("Types cannot be provided in put mapping requests");
+        if (request.getRestApiVersion() == RestApiVersion.V_7) {
+            final boolean includeTypeName = request.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER,
+                DEFAULT_INCLUDE_TYPE_NAME_POLICY);
+            if (request.hasParam(INCLUDE_TYPE_NAME_PARAMETER)) {
+                deprecationLogger.compatibleApiWarning("put_mapping_with_types", TYPES_DEPRECATION_MESSAGE);
+            }
+            final String type = request.param("type");
+            if (includeTypeName == false &&
+                (type != null || isMappingSourceTyped(MapperService.SINGLE_MAPPING_NAME, sourceAsMap))) {
+                throw new IllegalArgumentException("Types cannot be provided in put mapping requests, unless " +
+                    "the include_type_name parameter is set to true.");
+            }
+
+            Map<String, Object> mappingSource = prepareV7Mappings(includeTypeName, sourceAsMap);
+            putMappingRequest.source(mappingSource);
+        } else {
+            if (MapperService.isMappingSourceTyped(MapperService.SINGLE_MAPPING_NAME, sourceAsMap)) {
+                throw new IllegalArgumentException("Types cannot be provided in put mapping requests");
+            }
+            putMappingRequest.source(sourceAsMap);
         }
 
-        putMappingRequest.source(sourceAsMap);
         putMappingRequest.timeout(request.paramAsTime("timeout", putMappingRequest.timeout()));
         putMappingRequest.masterNodeTimeout(request.paramAsTime("master_timeout", putMappingRequest.masterNodeTimeout()));
         putMappingRequest.indicesOptions(IndicesOptions.fromRequest(request, putMappingRequest.indicesOptions()));
         putMappingRequest.writeIndexOnly(request.paramAsBoolean("write_index_only", false));
         return channel -> client.admin().indices().putMapping(putMappingRequest, new RestToXContentListener<>(channel));
     }
+
+    private Map<String, Object> prepareV7Mappings(boolean includeTypeName, Map<String, Object> mappings) {
+        if (includeTypeName && mappings != null && mappings.size() == 1) {
+            String typeName = mappings.keySet().iterator().next();
+            if (Strings.hasText(typeName) == false) {
+                throw new IllegalArgumentException("name cannot be empty string");
+            }
+            @SuppressWarnings("unchecked")
+            Map<String, Object> typelessMappings = (Map<String, Object>) mappings.get(typeName);
+            return typelessMappings;
+        }
+        return mappings;
+    }
+
 }