From 0103403c828ade38d9931ac9eb36926288329eb3 Mon Sep 17 00:00:00 2001
From: Bo Zhang <bzhangam@amazon.com>
Date: Tue, 25 Mar 2025 12:52:00 -0700
Subject: [PATCH] Introduce mapping transformer to allow transform mappings
 during index create/update or index template create/update.

Signed-off-by: Bo Zhang <bzhangam@amazon.com>
---
 CHANGELOG.md                                  |   3 +-
 .../create/TransportCreateIndexAction.java    |  51 +++++---
 .../put/TransportPutMappingAction.java        |  15 ++-
 .../TransportPutComponentTemplateAction.java  |  49 ++++++--
 ...sportPutComposableIndexTemplateAction.java |  48 ++++++--
 .../put/TransportPutIndexTemplateAction.java  |  51 ++++----
 .../MetadataIndexTemplateService.java         |   4 +
 .../opensearch/cluster/metadata/Template.java |   6 +-
 .../index/mapper/MappingTransformer.java      |  37 ++++++
 .../mapper/MappingTransformerRegistry.java    | 113 +++++++++++++++++
 .../main/java/org/opensearch/node/Node.java   |   7 +-
 .../org/opensearch/plugins/MapperPlugin.java  |  11 ++
 .../TransportCreateIndexActionTests.java      | 107 ++++++++++++++++
 .../put/TransportPutMappingActionTests.java   | 110 +++++++++++++++++
 ...nsportPutComponentTemplateActionTests.java | 111 +++++++++++++++++
 ...PutComposableIndexTemplateActionTests.java | 110 +++++++++++++++++
 .../TransportPutIndexTemplateActionTests.java | 105 ++++++++++++++++
 .../MappingTransformerRegistryTests.java      | 116 ++++++++++++++++++
 .../indices/cluster/ClusterStateChanges.java  |  20 ++-
 .../snapshots/SnapshotResiliencyTests.java    |  12 +-
 20 files changed, 1016 insertions(+), 70 deletions(-)
 create mode 100644 server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java
 create mode 100644 server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java
 create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java
 create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java
 create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java
 create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java
 create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java
 create mode 100644 server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59f8f9347402d..1567550c3a727 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,9 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 - [Rule Based Auto-tagging] Add rule schema for auto tagging ([#17238](https://github.com/opensearch-project/OpenSearch/pull/17238))
 - Renaming the node role search to warm ([#17573](https://github.com/opensearch-project/OpenSearch/pull/17573))
 - Introduce a new search node role to hold search only shards ([#17620](https://github.com/opensearch-project/OpenSearch/pull/17620))
-- Fix systemd integTest on deb regarding path ownership check ([#17641](https://github.com/opensearch-project/OpenSearch/pull/17641)) 
+- Fix systemd integTest on deb regarding path ownership check ([#17641](https://github.com/opensearch-project/OpenSearch/pull/17641))
 - Add dfs transformation function in XContentMapValues ([#17612](https://github.com/opensearch-project/OpenSearch/pull/17612))
 - Added Kinesis support as a plugin for the pull-based ingestion ([#17615](https://github.com/opensearch-project/OpenSearch/pull/17615)
+- Introduce mapping transformer to allow transform mappings during index create/update or index template create/update ([#17635](https://github.com/opensearch-project/OpenSearch/pull/17635))
 
 ### Changed
 - Migrate BC libs to their FIPS counterparts ([#14912](https://github.com/opensearch-project/OpenSearch/pull/14912))
diff --git a/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java b/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java
index 38dc1a418ec8b..dd0a815a5c497 100644
--- a/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java
+++ b/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java
@@ -43,6 +43,7 @@
 import org.opensearch.common.inject.Inject;
 import org.opensearch.core.action.ActionListener;
 import org.opensearch.core.common.io.stream.StreamInput;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.threadpool.ThreadPool;
 import org.opensearch.transport.TransportService;
 
@@ -56,6 +57,7 @@
 public class TransportCreateIndexAction extends TransportClusterManagerNodeAction<CreateIndexRequest, CreateIndexResponse> {
 
     private final MetadataCreateIndexService createIndexService;
+    private final MappingTransformerRegistry mappingTransformerRegistry;
 
     @Inject
     public TransportCreateIndexAction(
@@ -64,7 +66,9 @@ public TransportCreateIndexAction(
         ThreadPool threadPool,
         MetadataCreateIndexService createIndexService,
         ActionFilters actionFilters,
-        IndexNameExpressionResolver indexNameExpressionResolver
+        IndexNameExpressionResolver indexNameExpressionResolver,
+        MappingTransformerRegistry mappingTransformerRegistry
+
     ) {
         super(
             CreateIndexAction.NAME,
@@ -76,6 +80,7 @@ public TransportCreateIndexAction(
             indexNameExpressionResolver
         );
         this.createIndexService = createIndexService;
+        this.mappingTransformerRegistry = mappingTransformerRegistry;
     }
 
     @Override
@@ -112,25 +117,31 @@ protected void clusterManagerOperation(
         }
 
         final String indexName = indexNameExpressionResolver.resolveDateMathExpression(request.index());
-        final CreateIndexClusterStateUpdateRequest updateRequest = new CreateIndexClusterStateUpdateRequest(
-            cause,
-            indexName,
-            request.index()
-        ).ackTimeout(request.timeout())
-            .clusterManagerNodeTimeout(request.clusterManagerNodeTimeout())
-            .settings(request.settings())
-            .mappings(request.mappings())
-            .aliases(request.aliases())
-            .context(request.context())
-            .waitForActiveShards(request.waitForActiveShards());
-
-        createIndexService.createIndex(
-            updateRequest,
-            ActionListener.map(
-                listener,
-                response -> new CreateIndexResponse(response.isAcknowledged(), response.isShardsAcknowledged(), indexName)
-            )
-        );
+
+        final String finalCause = cause;
+        final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMappings -> {
+            final CreateIndexClusterStateUpdateRequest updateRequest = new CreateIndexClusterStateUpdateRequest(
+                finalCause,
+                indexName,
+                request.index()
+            ).ackTimeout(request.timeout())
+                .clusterManagerNodeTimeout(request.clusterManagerNodeTimeout())
+                .settings(request.settings())
+                .mappings(transformedMappings)
+                .aliases(request.aliases())
+                .context(request.context())
+                .waitForActiveShards(request.waitForActiveShards());
+
+            createIndexService.createIndex(
+                updateRequest,
+                ActionListener.map(
+                    listener,
+                    response -> new CreateIndexResponse(response.isAcknowledged(), response.isShardsAcknowledged(), indexName)
+                )
+            );
+        }, listener::onFailure);
+
+        mappingTransformerRegistry.applyTransformers(request.mappings(), null, mappingTransformListener);
     }
 
 }
diff --git a/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java b/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java
index ed936822bfdcd..4c37592714185 100644
--- a/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java
+++ b/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java
@@ -50,7 +50,9 @@
 import org.opensearch.core.action.ActionListener;
 import org.opensearch.core.common.io.stream.StreamInput;
 import org.opensearch.core.index.Index;
+import org.opensearch.core.xcontent.MediaTypeRegistry;
 import org.opensearch.index.IndexNotFoundException;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.threadpool.ThreadPool;
 import org.opensearch.transport.TransportService;
 
@@ -72,6 +74,7 @@ public class TransportPutMappingAction extends TransportClusterManagerNodeAction
 
     private final MetadataMappingService metadataMappingService;
     private final RequestValidators<PutMappingRequest> requestValidators;
+    private final MappingTransformerRegistry mappingTransformerRegistry;
 
     @Inject
     public TransportPutMappingAction(
@@ -81,7 +84,8 @@ public TransportPutMappingAction(
         final MetadataMappingService metadataMappingService,
         final ActionFilters actionFilters,
         final IndexNameExpressionResolver indexNameExpressionResolver,
-        final RequestValidators<PutMappingRequest> requestValidators
+        final RequestValidators<PutMappingRequest> requestValidators,
+        final MappingTransformerRegistry mappingTransformerRegistry
     ) {
         super(
             PutMappingAction.NAME,
@@ -94,6 +98,7 @@ public TransportPutMappingAction(
         );
         this.metadataMappingService = metadataMappingService;
         this.requestValidators = Objects.requireNonNull(requestValidators);
+        this.mappingTransformerRegistry = mappingTransformerRegistry;
     }
 
     @Override
@@ -132,7 +137,13 @@ protected void clusterManagerOperation(
                 listener.onFailure(maybeValidationException.get());
                 return;
             }
-            performMappingUpdate(concreteIndices, request, listener, metadataMappingService);
+
+            final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMapping -> {
+                request.source(transformedMapping, MediaTypeRegistry.JSON);
+                performMappingUpdate(concreteIndices, request, listener, metadataMappingService);
+            }, listener::onFailure);
+
+            mappingTransformerRegistry.applyTransformers(request.source(), null, mappingTransformListener);
         } catch (IndexNotFoundException ex) {
             logger.debug(() -> new ParameterizedMessage("failed to put mappings on indices [{}]", Arrays.asList(request.indices())), ex);
             throw ex;
diff --git a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java
index 66e2fe5c535db..a8002ec01d8cb 100644
--- a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java
+++ b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java
@@ -44,16 +44,20 @@
 import org.opensearch.cluster.metadata.MetadataIndexTemplateService;
 import org.opensearch.cluster.metadata.Template;
 import org.opensearch.cluster.service.ClusterService;
+import org.opensearch.common.compress.CompressedXContent;
 import org.opensearch.common.inject.Inject;
 import org.opensearch.common.settings.IndexScopedSettings;
 import org.opensearch.common.settings.Settings;
 import org.opensearch.core.action.ActionListener;
 import org.opensearch.core.common.io.stream.StreamInput;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.threadpool.ThreadPool;
 import org.opensearch.transport.TransportService;
 
 import java.io.IOException;
 
+import reactor.util.annotation.NonNull;
+
 /**
  * An action for putting a single component template into the cluster state
  *
@@ -65,6 +69,7 @@ public class TransportPutComponentTemplateAction extends TransportClusterManager
 
     private final MetadataIndexTemplateService indexTemplateService;
     private final IndexScopedSettings indexScopedSettings;
+    private final MappingTransformerRegistry mappingTransformerRegistry;
 
     @Inject
     public TransportPutComponentTemplateAction(
@@ -74,7 +79,8 @@ public TransportPutComponentTemplateAction(
         MetadataIndexTemplateService indexTemplateService,
         ActionFilters actionFilters,
         IndexNameExpressionResolver indexNameExpressionResolver,
-        IndexScopedSettings indexScopedSettings
+        IndexScopedSettings indexScopedSettings,
+        MappingTransformerRegistry mappingTransformerRegistry
     ) {
         super(
             PutComponentTemplateAction.NAME,
@@ -87,6 +93,7 @@ public TransportPutComponentTemplateAction(
         );
         this.indexTemplateService = indexTemplateService;
         this.indexScopedSettings = indexScopedSettings;
+        this.mappingTransformerRegistry = mappingTransformerRegistry;
     }
 
     @Override
@@ -121,13 +128,37 @@ protected void clusterManagerOperation(
             template = new Template(settings, template.mappings(), template.aliases());
             componentTemplate = new ComponentTemplate(template, componentTemplate.version(), componentTemplate.metadata());
         }
-        indexTemplateService.putComponentTemplate(
-            request.cause(),
-            request.create(),
-            request.name(),
-            request.clusterManagerNodeTimeout(),
-            componentTemplate,
-            listener
-        );
+
+        final ActionListener<String> mappingTransformListener = getMappingTransformListener(request, listener, componentTemplate);
+
+        transformMapping(template, mappingTransformListener);
+    }
+
+    private ActionListener<String> getMappingTransformListener(
+        @NonNull final PutComponentTemplateAction.Request request,
+        @NonNull final ActionListener<AcknowledgedResponse> listener,
+        @NonNull final ComponentTemplate componentTemplate
+    ) {
+        return ActionListener.wrap(transformedMappings -> {
+            if (transformedMappings != null && componentTemplate.template() != null) {
+                componentTemplate.template().setMappings(new CompressedXContent(transformedMappings));
+            }
+            indexTemplateService.putComponentTemplate(
+                request.cause(),
+                request.create(),
+                request.name(),
+                request.clusterManagerNodeTimeout(),
+                componentTemplate,
+                listener
+            );
+        }, listener::onFailure);
+    }
+
+    private void transformMapping(final Template template, @NonNull final ActionListener<String> mappingTransformListener) {
+        if (template == null || template.mappings() == null) {
+            mappingTransformListener.onResponse(null);
+        } else {
+            mappingTransformerRegistry.applyTransformers(template.mappings().string(), null, mappingTransformListener);
+        }
     }
 }
diff --git a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java
index a5c3590a0a6d7..5554a540a57f2 100644
--- a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java
+++ b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java
@@ -41,15 +41,20 @@
 import org.opensearch.cluster.metadata.ComposableIndexTemplate;
 import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
 import org.opensearch.cluster.metadata.MetadataIndexTemplateService;
+import org.opensearch.cluster.metadata.Template;
 import org.opensearch.cluster.service.ClusterService;
+import org.opensearch.common.compress.CompressedXContent;
 import org.opensearch.common.inject.Inject;
 import org.opensearch.core.action.ActionListener;
 import org.opensearch.core.common.io.stream.StreamInput;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.threadpool.ThreadPool;
 import org.opensearch.transport.TransportService;
 
 import java.io.IOException;
 
+import reactor.util.annotation.NonNull;
+
 /**
  * An action for putting a composable index template into the cluster state
  *
@@ -60,6 +65,7 @@ public class TransportPutComposableIndexTemplateAction extends TransportClusterM
     AcknowledgedResponse> {
 
     private final MetadataIndexTemplateService indexTemplateService;
+    private final MappingTransformerRegistry mappingTransformerRegistry;
 
     @Inject
     public TransportPutComposableIndexTemplateAction(
@@ -68,7 +74,8 @@ public TransportPutComposableIndexTemplateAction(
         ThreadPool threadPool,
         MetadataIndexTemplateService indexTemplateService,
         ActionFilters actionFilters,
-        IndexNameExpressionResolver indexNameExpressionResolver
+        IndexNameExpressionResolver indexNameExpressionResolver,
+        MappingTransformerRegistry mappingTransformerRegistry
     ) {
         super(
             PutComposableIndexTemplateAction.NAME,
@@ -80,6 +87,7 @@ public TransportPutComposableIndexTemplateAction(
             indexNameExpressionResolver
         );
         this.indexTemplateService = indexTemplateService;
+        this.mappingTransformerRegistry = mappingTransformerRegistry;
     }
 
     @Override
@@ -103,15 +111,35 @@ protected void clusterManagerOperation(
         final PutComposableIndexTemplateAction.Request request,
         final ClusterState state,
         final ActionListener<AcknowledgedResponse> listener
+    ) throws IOException {
+        final ComposableIndexTemplate indexTemplate = request.indexTemplate();
+
+        final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMappings -> {
+            if (transformedMappings != null && indexTemplate.template() != null) {
+                indexTemplate.template().setMappings(new CompressedXContent(transformedMappings));
+            }
+            indexTemplateService.putIndexTemplateV2(
+                request.cause(),
+                request.create(),
+                request.name(),
+                request.clusterManagerNodeTimeout(),
+                indexTemplate,
+                listener
+            );
+        }, listener::onFailure);
+
+        transformMapping(indexTemplate, mappingTransformListener);
+    }
+
+    private void transformMapping(
+        @NonNull final ComposableIndexTemplate indexTemplate,
+        @NonNull final ActionListener<String> mappingTransformListener
     ) {
-        ComposableIndexTemplate indexTemplate = request.indexTemplate();
-        indexTemplateService.putIndexTemplateV2(
-            request.cause(),
-            request.create(),
-            request.name(),
-            request.clusterManagerNodeTimeout(),
-            indexTemplate,
-            listener
-        );
+        final Template template = indexTemplate.template();
+        if (template == null || template.mappings() == null) {
+            mappingTransformListener.onResponse(null);
+        } else {
+            mappingTransformerRegistry.applyTransformers(template.mappings().string(), null, mappingTransformListener);
+        }
     }
 }
diff --git a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java
index b9f27c00d0d98..2467fc0346313 100644
--- a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java
+++ b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java
@@ -49,6 +49,7 @@
 import org.opensearch.common.settings.Settings;
 import org.opensearch.core.action.ActionListener;
 import org.opensearch.core.common.io.stream.StreamInput;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.threadpool.ThreadPool;
 import org.opensearch.transport.TransportService;
 
@@ -65,6 +66,7 @@ public class TransportPutIndexTemplateAction extends TransportClusterManagerNode
 
     private final MetadataIndexTemplateService indexTemplateService;
     private final IndexScopedSettings indexScopedSettings;
+    private final MappingTransformerRegistry mappingTransformerRegistry;
 
     @Inject
     public TransportPutIndexTemplateAction(
@@ -74,7 +76,8 @@ public TransportPutIndexTemplateAction(
         MetadataIndexTemplateService indexTemplateService,
         ActionFilters actionFilters,
         IndexNameExpressionResolver indexNameExpressionResolver,
-        IndexScopedSettings indexScopedSettings
+        IndexScopedSettings indexScopedSettings,
+        MappingTransformerRegistry mappingTransformerRegistry
     ) {
         super(
             PutIndexTemplateAction.NAME,
@@ -87,6 +90,7 @@ public TransportPutIndexTemplateAction(
         );
         this.indexTemplateService = indexTemplateService;
         this.indexScopedSettings = indexScopedSettings;
+        this.mappingTransformerRegistry = mappingTransformerRegistry;
     }
 
     @Override
@@ -118,28 +122,33 @@ protected void clusterManagerOperation(
         final Settings.Builder templateSettingsBuilder = Settings.builder();
         templateSettingsBuilder.put(request.settings()).normalizePrefix(IndexMetadata.INDEX_SETTING_PREFIX);
         indexScopedSettings.validate(templateSettingsBuilder.build(), true); // templates must be consistent with regards to dependencies
-        indexTemplateService.putTemplate(
-            new MetadataIndexTemplateService.PutRequest(cause, request.name()).patterns(request.patterns())
-                .order(request.order())
-                .settings(templateSettingsBuilder.build())
-                .mappings(request.mappings())
-                .aliases(request.aliases())
-                .create(request.create())
-                .clusterManagerTimeout(request.clusterManagerNodeTimeout())
-                .version(request.version()),
+        final String finalCause = cause;
+        final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMappings -> {
+            indexTemplateService.putTemplate(
+                new MetadataIndexTemplateService.PutRequest(finalCause, request.name()).patterns(request.patterns())
+                    .order(request.order())
+                    .settings(templateSettingsBuilder.build())
+                    .mappings(transformedMappings)
+                    .aliases(request.aliases())
+                    .create(request.create())
+                    .clusterManagerTimeout(request.clusterManagerNodeTimeout())
+                    .version(request.version()),
 
-            new MetadataIndexTemplateService.PutListener() {
-                @Override
-                public void onResponse(MetadataIndexTemplateService.PutResponse response) {
-                    listener.onResponse(new AcknowledgedResponse(response.acknowledged()));
-                }
+                new MetadataIndexTemplateService.PutListener() {
+                    @Override
+                    public void onResponse(MetadataIndexTemplateService.PutResponse response) {
+                        listener.onResponse(new AcknowledgedResponse(response.acknowledged()));
+                    }
 
-                @Override
-                public void onFailure(Exception e) {
-                    logger.debug(() -> new ParameterizedMessage("failed to put template [{}]", request.name()), e);
-                    listener.onFailure(e);
+                    @Override
+                    public void onFailure(Exception e) {
+                        logger.debug(() -> new ParameterizedMessage("failed to put template [{}]", request.name()), e);
+                        listener.onFailure(e);
+                    }
                 }
-            }
-        );
+            );
+        }, listener::onFailure);
+
+        mappingTransformerRegistry.applyTransformers(request.mappings(), null, mappingTransformListener);
     }
 }
diff --git a/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java
index b032ade720612..de106d14c6fd9 100644
--- a/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java
+++ b/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java
@@ -1754,6 +1754,10 @@ public PutRequest mappings(String mappings) {
             return this;
         }
 
+        public String getMappings() {
+            return mappings;
+        }
+
         public PutRequest aliases(Set<Alias> aliases) {
             this.aliases.addAll(aliases);
             return this;
diff --git a/server/src/main/java/org/opensearch/cluster/metadata/Template.java b/server/src/main/java/org/opensearch/cluster/metadata/Template.java
index bd110c6af8975..be29a73e9c0ad 100644
--- a/server/src/main/java/org/opensearch/cluster/metadata/Template.java
+++ b/server/src/main/java/org/opensearch/cluster/metadata/Template.java
@@ -94,7 +94,7 @@ public class Template extends AbstractDiffable<Template> implements ToXContentOb
     @Nullable
     private final Settings settings;
     @Nullable
-    private final CompressedXContent mappings;
+    private CompressedXContent mappings;
     @Nullable
     private final Map<String, AliasMetadata> aliases;
 
@@ -218,4 +218,8 @@ private static Map<String, Object> reduceMapping(Map<String, Object> mapping) {
             return mapping;
         }
     }
+
+    public void setMappings(CompressedXContent mappings) {
+        this.mappings = mappings;
+    }
 }
diff --git a/server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java b/server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java
new file mode 100644
index 0000000000000..82e48accd08a1
--- /dev/null
+++ b/server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java
@@ -0,0 +1,37 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.index.mapper;
+
+import org.opensearch.core.action.ActionListener;
+
+import java.util.Map;
+
+import reactor.util.annotation.NonNull;
+
+/**
+ * A transformer to allow plugins to implement logic to transform the index mapping during
+ * index creation/update and index template creation/update on transport layer.
+ *
+ */
+public interface MappingTransformer {
+    default void transform(
+        final Map<String, Object> mapping,
+        final TransformContext context,
+        @NonNull final ActionListener<Void> listener
+    ) {
+        listener.onResponse(null);
+    }
+
+    /**
+     * Context for mapping transform. For now, we don't need any context, but it's defined for future scalability.
+     * It can be used to provide the info like we are transforming the mapping for what transport action. Or provide
+     * index setting info to help transform the mapping.
+     */
+    class TransformContext {}
+}
diff --git a/server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java b/server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java
new file mode 100644
index 0000000000000..192699edf5e4f
--- /dev/null
+++ b/server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java
@@ -0,0 +1,113 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.index.mapper;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.opensearch.common.xcontent.XContentFactory;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.core.xcontent.NamedXContentRegistry;
+import org.opensearch.core.xcontent.XContentBuilder;
+import org.opensearch.plugins.MapperPlugin;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import reactor.util.annotation.NonNull;
+
+/**
+ * This class collects all registered mapping transformers and applies them when needed.
+ */
+public class MappingTransformerRegistry {
+    private final List<MappingTransformer> transformers;
+    private final NamedXContentRegistry xContentRegistry;
+    protected final Logger logger = LogManager.getLogger(getClass());
+
+    public MappingTransformerRegistry(
+        @NonNull final List<MapperPlugin> mapperPlugins,
+        @NonNull final NamedXContentRegistry xContentRegistry
+    ) {
+        this.xContentRegistry = xContentRegistry;
+        this.transformers = new ArrayList<>();
+        // Collect transformers from all MapperPlugins
+        for (MapperPlugin plugin : mapperPlugins) {
+            transformers.addAll(plugin.getMappingTransformers());
+        }
+    }
+
+    private void applyNext(
+        @NonNull final Map<String, Object> mapping,
+        final MappingTransformer.TransformContext context,
+        @NonNull final AtomicInteger index,
+        @NonNull final ActionListener<String> listener
+    ) {
+        if (index.get() == transformers.size()) {
+            try {
+                final XContentBuilder builder = XContentFactory.jsonBuilder();
+                builder.map(mapping);
+                listener.onResponse(builder.toString());
+            } catch (IOException e) {
+                listener.onFailure(new RuntimeException("Failed to parse the transformed mapping [" + mapping + "]", e));
+            }
+            return;
+        }
+
+        final MappingTransformer transformer = transformers.get(index.getAndIncrement());
+        logger.debug("Applying mapping transformer: {}", transformer.getClass().getName());
+        transformer.transform(mapping, context, new ActionListener<>() {
+            @Override
+            public void onResponse(Void unused) {
+                logger.debug("Completed transformation: {}", transformer.getClass().getName());
+                applyNext(mapping, context, index, listener);
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                logger.error("Transformer {} failed: {}", transformer.getClass().getName(), e.getMessage());
+                listener.onFailure(e);
+            }
+        });
+    }
+
+    /**
+     * Applies all registered transformers to the provided mapping which is in string format.
+     * @param mappingString The index mapping to transform.
+     * @param context Context for mapping transform
+     * @param mappingTransformListener A listener for mapping transform
+     */
+    public void applyTransformers(
+        final String mappingString,
+        final MappingTransformer.TransformContext context,
+        @NonNull final ActionListener<String> mappingTransformListener
+    ) {
+        if (transformers.isEmpty() || mappingString == null) {
+            mappingTransformListener.onResponse(mappingString);
+            return;
+        }
+
+        Map<String, Object> mappingMap;
+        try {
+            mappingMap = MapperService.parseMapping(xContentRegistry, mappingString);
+        } catch (IOException e) {
+            mappingTransformListener.onFailure(
+                new IllegalArgumentException(
+                    "Failed to transform the mappings because failed to parse the mappings [" + mappingString + "]",
+                    e
+                )
+            );
+            return;
+        }
+
+        final AtomicInteger index = new AtomicInteger(0);
+        applyNext(mappingMap, context, index, mappingTransformListener);
+    }
+}
diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java
index 8037f90653d89..fadc33515bc27 100644
--- a/server/src/main/java/org/opensearch/node/Node.java
+++ b/server/src/main/java/org/opensearch/node/Node.java
@@ -153,6 +153,7 @@
 import org.opensearch.index.analysis.AnalysisRegistry;
 import org.opensearch.index.compositeindex.CompositeIndexSettings;
 import org.opensearch.index.engine.EngineFactory;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.index.recovery.RemoteStoreRestoreService;
 import org.opensearch.index.remote.RemoteIndexPathUploader;
 import org.opensearch.index.remote.RemoteStoreStatsTrackerFactory;
@@ -762,7 +763,8 @@ protected Node(
                 clusterManagerMetrics
             );
             modules.add(clusterModule);
-            IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class));
+            final List<MapperPlugin> mapperPlugins = pluginsService.filterPlugins(MapperPlugin.class);
+            IndicesModule indicesModule = new IndicesModule(mapperPlugins);
             modules.add(indicesModule);
 
             SearchModule searchModule = new SearchModule(settings, pluginsService.filterPlugins(SearchPlugin.class));
@@ -1433,6 +1435,8 @@ protected Node(
             resourcesToClose.add(persistentTasksClusterService);
             final PersistentTasksService persistentTasksService = new PersistentTasksService(clusterService, threadPool, client);
 
+            final MappingTransformerRegistry mappingTransformerRegistry = new MappingTransformerRegistry(mapperPlugins, xContentRegistry);
+
             modules.add(b -> {
                 b.bind(Node.class).toInstance(this);
                 b.bind(NodeService.class).toInstance(nodeService);
@@ -1534,6 +1538,7 @@ protected Node(
                 b.bind(SegmentReplicationStatsTracker.class).toInstance(segmentReplicationStatsTracker);
                 b.bind(SearchRequestOperationsCompositeListenerFactory.class).toInstance(searchRequestOperationsCompositeListenerFactory);
                 b.bind(SegmentReplicator.class).toInstance(segmentReplicator);
+                b.bind(MappingTransformerRegistry.class).toInstance(mappingTransformerRegistry);
 
                 taskManagerClientOptional.ifPresent(value -> b.bind(TaskManagerClient.class).toInstance(value));
             });
diff --git a/server/src/main/java/org/opensearch/plugins/MapperPlugin.java b/server/src/main/java/org/opensearch/plugins/MapperPlugin.java
index ff6daf1b720f6..ef5b3bf2f574c 100644
--- a/server/src/main/java/org/opensearch/plugins/MapperPlugin.java
+++ b/server/src/main/java/org/opensearch/plugins/MapperPlugin.java
@@ -33,9 +33,11 @@
 package org.opensearch.plugins;
 
 import org.opensearch.index.mapper.Mapper;
+import org.opensearch.index.mapper.MappingTransformer;
 import org.opensearch.index.mapper.MetadataFieldMapper;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -90,4 +92,13 @@ default Function<String, Predicate<String>> getFieldFilter() {
      * get field mappings and field capabilities API will return every field that's present in the mappings.
      */
     Function<String, Predicate<String>> NOOP_FIELD_FILTER = index -> NOOP_FIELD_PREDICATE;
+
+    /**
+     * Returns mapper transformer implementations added by this plugin.
+     *
+     */
+    default List<MappingTransformer> getMappingTransformers() {
+        return Collections.emptyList();
+    }
+
 }
diff --git a/server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java
new file mode 100644
index 0000000000000..5b5c8e5954157
--- /dev/null
+++ b/server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java
@@ -0,0 +1,107 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.action.admin.indices.create;
+
+import org.opensearch.action.support.ActionFilter;
+import org.opensearch.action.support.ActionFilters;
+import org.opensearch.cluster.ClusterState;
+import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
+import org.opensearch.cluster.metadata.MetadataCreateIndexService;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
+import org.opensearch.test.OpenSearchTestCase;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.TransportService;
+import org.junit.Before;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TransportCreateIndexActionTests extends OpenSearchTestCase {
+    @Mock
+    private TransportService transportService;
+    @Mock
+    private ActionFilters actionFilters;
+    @Mock
+    private ThreadPool threadPool;
+    @Mock
+    private MetadataCreateIndexService createIndexService;
+    @Mock
+    private IndexNameExpressionResolver indexNameExpressionResolver;
+
+    @Mock
+    private MappingTransformerRegistry mappingTransformerRegistry;
+
+    @Mock
+    private ClusterState clusterState;
+
+    @Mock
+    private ActionListener<CreateIndexResponse> responseListener;
+
+    private TransportCreateIndexAction action;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+
+        ActionFilter[] emptyActionFilters = new ActionFilter[] {};
+        when(actionFilters.filters()).thenReturn(emptyActionFilters);
+        action = new TransportCreateIndexAction(
+            transportService,
+            null, // ClusterService not needed for this test
+            threadPool,
+            createIndexService,
+            actionFilters,
+            indexNameExpressionResolver,
+            mappingTransformerRegistry
+        );
+    }
+
+    public void testClusterManagerOperation_usesTransformedMapping() {
+        when(indexNameExpressionResolver.resolveDateMathExpression(any())).thenReturn("test-index");
+
+        // Arrange: Create a test request
+        final CreateIndexRequest request = new CreateIndexRequest("test-index");
+        request.mapping("{\"properties\": {\"field\": {\"type\": \"text\"}}}");
+
+        // Mock transformed mapping result
+        final String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}";
+
+        // Capture ActionListener passed to applyTransformers
+        final ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        doNothing().when(mappingTransformerRegistry).applyTransformers(
+            anyString(),
+            any(),
+            listenerCaptor.capture()
+        );
+
+        // Act: Call the method
+        action.clusterManagerOperation(request, clusterState, responseListener);
+
+        // Simulate transformation completion
+        listenerCaptor.getValue().onResponse(transformedMapping);
+
+        // Assert: Capture request sent to createIndexService
+        ArgumentCaptor<CreateIndexClusterStateUpdateRequest> updateRequestCaptor =
+            ArgumentCaptor.forClass(CreateIndexClusterStateUpdateRequest.class);
+        verify(createIndexService, times(1)).createIndex(updateRequestCaptor.capture(), any());
+
+        // Ensure transformed mapping is passed correctly
+        CreateIndexClusterStateUpdateRequest capturedRequest = updateRequestCaptor.getValue();
+        assertEquals(transformedMapping, capturedRequest.mappings());
+    }
+}
diff --git a/server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java
new file mode 100644
index 0000000000000..f1ed0a09230bf
--- /dev/null
+++ b/server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java
@@ -0,0 +1,110 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.action.admin.indices.mapping.put;
+
+import org.opensearch.action.RequestValidators;
+import org.opensearch.action.support.ActionFilter;
+import org.opensearch.action.support.ActionFilters;
+import org.opensearch.action.support.clustermanager.AcknowledgedResponse;
+import org.opensearch.cluster.ClusterState;
+import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
+import org.opensearch.cluster.metadata.MetadataMappingService;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.core.xcontent.MediaTypeRegistry;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
+import org.opensearch.test.OpenSearchTestCase;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.TransportService;
+import org.junit.Before;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TransportPutMappingActionTests extends OpenSearchTestCase {
+    @Mock
+    private TransportService transportService;
+    @Mock
+    private ActionFilters actionFilters;
+    @Mock
+    private ThreadPool threadPool;
+    @Mock
+    private IndexNameExpressionResolver indexNameExpressionResolver;
+
+    @Mock
+    private MetadataMappingService metadataMappingService;
+
+    @Mock
+    private MappingTransformerRegistry mappingTransformerRegistry;
+
+    @Mock
+    private RequestValidators<PutMappingRequest> requestValidators;
+
+    @Mock
+    private ClusterState clusterState;
+
+    @Mock
+    private ActionListener<AcknowledgedResponse> responseListener;
+
+    private TransportPutMappingAction action;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+
+        ActionFilter[] emptyActionFilters = new ActionFilter[] {};
+        when(actionFilters.filters()).thenReturn(emptyActionFilters);
+        action = new TransportPutMappingAction(
+            transportService,
+            null, // ClusterService not needed for this test
+            threadPool,
+            metadataMappingService,
+            actionFilters,
+            indexNameExpressionResolver,
+            requestValidators,
+            mappingTransformerRegistry
+        );
+    }
+
+    public void testClusterManagerOperation_transformedMappingUsed() {
+        // Arrange: Create a test request
+        final PutMappingRequest request = new PutMappingRequest("index");
+        final String originalMapping = "{\"properties\": {\"field\": {\"type\": \"text\"}}}";
+        request.source(originalMapping, MediaTypeRegistry.JSON);
+
+        String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}";
+
+        // Mock the transformer registry to return the transformed mapping
+        ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture());
+
+        // Act: Call the method
+        action.clusterManagerOperation(request, clusterState, responseListener);
+
+        // Simulate transformation completion
+        listenerCaptor.getValue().onResponse(transformedMapping);
+
+        // Assert: Verify the transformed mapping is passed to metadataMappingService
+        ArgumentCaptor<PutMappingClusterStateUpdateRequest> updateRequestCaptor = ArgumentCaptor.forClass(
+            PutMappingClusterStateUpdateRequest.class
+        );
+        verify(metadataMappingService, times(1)).putMapping(updateRequestCaptor.capture(), any());
+
+        // Ensure the transformed mapping is used correctly
+        PutMappingClusterStateUpdateRequest capturedRequest = updateRequestCaptor.getValue();
+        assertEquals(transformedMapping, capturedRequest.source());
+    }
+}
diff --git a/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java
new file mode 100644
index 0000000000000..1c84eac7cd093
--- /dev/null
+++ b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java
@@ -0,0 +1,111 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.action.admin.indices.template.put;
+
+import org.opensearch.action.support.ActionFilter;
+import org.opensearch.action.support.ActionFilters;
+import org.opensearch.action.support.clustermanager.AcknowledgedResponse;
+import org.opensearch.cluster.ClusterState;
+import org.opensearch.cluster.metadata.ComponentTemplate;
+import org.opensearch.cluster.metadata.MetadataIndexTemplateService;
+import org.opensearch.cluster.metadata.Template;
+import org.opensearch.common.compress.CompressedXContent;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
+import org.opensearch.test.OpenSearchTestCase;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.TransportService;
+import org.junit.Before;
+
+import java.io.IOException;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TransportPutComponentTemplateActionTests extends OpenSearchTestCase {
+    @Mock
+    private TransportService transportService;
+    @Mock
+    private ActionFilters actionFilters;
+    @Mock
+    private ThreadPool threadPool;
+    @Mock
+    private MappingTransformerRegistry mappingTransformerRegistry;
+    @Mock
+    private MetadataIndexTemplateService indexTemplateService;
+    @Mock
+    private ActionListener<AcknowledgedResponse> responseListener;
+    @Mock
+    private ClusterState clusterState;
+
+    private TransportPutComponentTemplateAction action;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+
+        ActionFilter[] emptyActionFilters = new ActionFilter[] {};
+        when(actionFilters.filters()).thenReturn(emptyActionFilters);
+        action = new TransportPutComponentTemplateAction(
+            transportService,
+            null, // ClusterService not needed for this test
+            threadPool,
+            indexTemplateService,
+            actionFilters,
+            null, // IndexNameExpressionResolver not needed
+            null, // IndexScopedSettings not needed
+            mappingTransformerRegistry
+        );
+    }
+
+    public void testClusterManagerOperation_mappingTransformationApplied() throws IOException {
+        // Arrange: Create a test request and mock dependencies
+        PutComponentTemplateAction.Request request = new PutComponentTemplateAction.Request("test");
+        ComponentTemplate componentTemplate = mock(ComponentTemplate.class);
+        Template template = mock(Template.class);
+        when(componentTemplate.template()).thenReturn(template);
+        when(template.mappings()).thenReturn(new CompressedXContent("{\"properties\": {\"field\": {\"type\": \"text\"}}}"));
+        request.componentTemplate(componentTemplate);
+
+        String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}";
+
+        // Mock mapping transformer to return transformed mapping
+        ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture());
+
+        // Act: Call the method
+        action.clusterManagerOperation(request, clusterState, responseListener);
+
+        // Simulate mapping transformation
+        listenerCaptor.getValue().onResponse(transformedMapping);
+
+        // Assert: Verify the transformed mappings are set correctly
+        verify(template, times(1)).setMappings(new CompressedXContent(transformedMapping));
+
+        // Verify that indexTemplateService.putComponentTemplate is called
+        verify(indexTemplateService, times(1)).putComponentTemplate(
+            eq(request.cause()),
+            eq(request.create()),
+            eq(request.name()),
+            eq(request.clusterManagerNodeTimeout()),
+            eq(componentTemplate),
+            eq(responseListener)
+        );
+    }
+}
diff --git a/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java
new file mode 100644
index 0000000000000..db58639ee6cdc
--- /dev/null
+++ b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java
@@ -0,0 +1,110 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.action.admin.indices.template.put;
+
+import org.opensearch.action.support.ActionFilter;
+import org.opensearch.action.support.ActionFilters;
+import org.opensearch.action.support.clustermanager.AcknowledgedResponse;
+import org.opensearch.cluster.ClusterState;
+import org.opensearch.cluster.metadata.ComposableIndexTemplate;
+import org.opensearch.cluster.metadata.MetadataIndexTemplateService;
+import org.opensearch.cluster.metadata.Template;
+import org.opensearch.common.compress.CompressedXContent;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
+import org.opensearch.test.OpenSearchTestCase;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.TransportService;
+import org.junit.Before;
+
+import java.io.IOException;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TransportPutComposableIndexTemplateActionTests extends OpenSearchTestCase {
+    @Mock
+    private TransportService transportService;
+    @Mock
+    private ActionFilters actionFilters;
+    @Mock
+    private ThreadPool threadPool;
+
+    @Mock
+    private MappingTransformerRegistry mappingTransformerRegistry;
+    @Mock
+    private MetadataIndexTemplateService indexTemplateService;
+    @Mock
+    private ActionListener<AcknowledgedResponse> responseListener;
+    @Mock
+    private ClusterState clusterState;
+
+    private TransportPutComposableIndexTemplateAction action;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+
+        ActionFilter[] emptyActionFilters = new ActionFilter[] {};
+        when(actionFilters.filters()).thenReturn(emptyActionFilters);
+        action = new TransportPutComposableIndexTemplateAction(
+            transportService,
+            null, // ClusterService not needed for this test
+            threadPool,
+            indexTemplateService,
+            actionFilters,
+            null, // IndexNameExpressionResolver not needed
+            mappingTransformerRegistry
+        );
+    }
+
+    public void testClusterManagerOperation_mappingTransformationApplied() throws IOException {
+        // Arrange: Create a test request and mock dependencies
+        PutComposableIndexTemplateAction.Request request = new PutComposableIndexTemplateAction.Request("test");
+        ComposableIndexTemplate indexTemplate = mock(ComposableIndexTemplate.class);
+        Template template = mock(Template.class);
+        String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}";
+        when(indexTemplate.template()).thenReturn(template);
+        when(template.mappings()).thenReturn(new CompressedXContent("{\"properties\": {\"field\": {\"type\": \"text\"}}}"));
+        request.indexTemplate(indexTemplate);
+
+        // Mock mapping transformer to return transformed mapping
+        ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture());
+
+        // Act: Call the method
+        action.clusterManagerOperation(request, clusterState, responseListener);
+
+        // Simulate mapping transformation
+        listenerCaptor.getValue().onResponse(transformedMapping);
+
+        // Assert: Verify the transformed mappings are set correctly
+        verify(template, times(1)).setMappings(new CompressedXContent(transformedMapping));
+
+        // Verify that indexTemplateService.putIndexTemplateV2 is called
+        verify(indexTemplateService, times(1)).putIndexTemplateV2(
+            eq(request.cause()),
+            eq(request.create()),
+            eq(request.name()),
+            eq(request.clusterManagerNodeTimeout()),
+            eq(indexTemplate),
+            eq(responseListener)
+        );
+    }
+}
diff --git a/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java
new file mode 100644
index 0000000000000..4ba5cf066cd66
--- /dev/null
+++ b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java
@@ -0,0 +1,105 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.action.admin.indices.template.put;
+
+import org.opensearch.action.support.ActionFilter;
+import org.opensearch.action.support.ActionFilters;
+import org.opensearch.action.support.clustermanager.AcknowledgedResponse;
+import org.opensearch.cluster.ClusterState;
+import org.opensearch.cluster.metadata.MetadataIndexTemplateService;
+import org.opensearch.common.settings.IndexScopedSettings;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.core.xcontent.MediaTypeRegistry;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
+import org.opensearch.test.OpenSearchTestCase;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.TransportService;
+import org.junit.Before;
+
+import java.util.Collections;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TransportPutIndexTemplateActionTests extends OpenSearchTestCase {
+    @Mock
+    private TransportService transportService;
+    @Mock
+    private ActionFilters actionFilters;
+    @Mock
+    private ThreadPool threadPool;
+
+    @Mock
+    private MappingTransformerRegistry mappingTransformerRegistry;
+    @Mock
+    private MetadataIndexTemplateService indexTemplateService;
+    @Mock
+    private ActionListener<AcknowledgedResponse> responseListener;
+    @Mock
+    private ClusterState clusterState;
+
+    private final IndexScopedSettings indexScopedSettings = new IndexScopedSettings(Settings.EMPTY, Collections.emptySet());
+
+    private TransportPutIndexTemplateAction action;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+
+        ActionFilter[] emptyActionFilters = new ActionFilter[] {};
+        when(actionFilters.filters()).thenReturn(emptyActionFilters);
+        action = new TransportPutIndexTemplateAction(
+            transportService,
+            null, // ClusterService not needed for this test
+            threadPool,
+            indexTemplateService,
+            actionFilters,
+            null, // IndexNameExpressionResolver not needed
+            indexScopedSettings,
+            mappingTransformerRegistry
+        );
+    }
+
+    public void testClusterManagerOperation_mappingTransformationApplied() {
+        // Arrange: Mock the request and response
+        PutIndexTemplateRequest request = new PutIndexTemplateRequest("test");
+        String originalMappings = "{\"properties\":{\"field\":{\"type\":\"text\"}}}";
+        request.mapping(originalMappings, MediaTypeRegistry.JSON);
+
+        // Mock the transformed mapping
+        String transformedMappings = "{\"properties\":{\"field\":{\"type\":\"keyword\"}}}";
+
+        // Mock mapping transformer to return transformed mapping
+        ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture());
+
+        // Act: Call the method
+        action.clusterManagerOperation(request, clusterState, responseListener);
+
+        // Simulate mapping transformation
+        listenerCaptor.getValue().onResponse(transformedMappings);
+
+        // Assert: Verify that the transformed mappings are passed to the template service
+        ArgumentCaptor<MetadataIndexTemplateService.PutRequest> putRequestCaptor = ArgumentCaptor.forClass(
+            MetadataIndexTemplateService.PutRequest.class
+        );
+        verify(indexTemplateService).putTemplate(putRequestCaptor.capture(), any());
+
+        // Verify the transformed mappings are set in the PutRequest
+        assertEquals(transformedMappings, putRequestCaptor.getValue().getMappings());
+    }
+}
diff --git a/server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java b/server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java
new file mode 100644
index 0000000000000..4c5ad09858921
--- /dev/null
+++ b/server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java
@@ -0,0 +1,116 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.index.mapper;
+
+import joptsimple.internal.Strings;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.core.xcontent.NamedXContentRegistry;
+import org.opensearch.plugins.MapperPlugin;
+import org.opensearch.test.OpenSearchTestCase;
+import org.junit.Before;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class MappingTransformerRegistryTests extends OpenSearchTestCase {
+    @Mock
+    private MapperPlugin mapperPlugin;
+    @Mock
+    private NamedXContentRegistry xContentRegistry;
+    @Mock
+    private ActionListener<String> listener;
+    @Mock
+    private MappingTransformer transformer;
+
+    private MappingTransformerRegistry registry;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+        when(mapperPlugin.getMappingTransformers()).thenReturn(List.of(transformer));
+        registry = new MappingTransformerRegistry(List.of(mapperPlugin), xContentRegistry);
+        // Mock dummy transformer behavior which does not modify the mapping
+        doAnswer(invocation -> {
+            final ActionListener<Void> actionListener = invocation.getArgument(2);
+            actionListener.onResponse(null);
+            return null;
+        }).when(transformer).transform(any(), any(), any());
+    }
+
+    public void testApplyTransformers_whenNoTransformers_returnMappingDirectly() {
+        final String mapping = Strings.EMPTY;
+        final MappingTransformerRegistry mappingTransformerRegistry = new MappingTransformerRegistry(
+            Collections.emptyList(),
+            xContentRegistry
+        );
+
+        mappingTransformerRegistry.applyTransformers(mapping, null, listener);
+
+        verify(listener, Mockito.times(1)).onResponse(mapping);
+    }
+
+    public void testApplyTransformers_WithNullMapping_ShouldReturnNull() {
+        registry.applyTransformers(null, null, listener);
+
+        verify(listener).onResponse(null);
+    }
+
+    public void testApplyTransformers_WithTransformerApplied() throws IOException {
+        final String inputMappingString = "{\"field\": \"value\"}";
+        final String expectedTransformedMappingString = "{\"field\":\"transformedValue\"}";
+
+        doAnswer(invocation -> {
+            Map<String, Object> mapping = invocation.getArgument(0);
+            assertEquals(mapping.get("field"), "value");
+            mapping.put("field", "transformedValue"); // Simulating transformation
+            ActionListener<Void> actionListener = invocation.getArgument(2);
+            actionListener.onResponse(null);
+            return null;
+        }).when(transformer).transform(any(), any(), any());
+
+        registry.applyTransformers(inputMappingString, null, listener);
+
+        ArgumentCaptor<String> responseCaptor = ArgumentCaptor.forClass(String.class);
+        verify(listener).onResponse(responseCaptor.capture());
+
+        String transformedMapping = responseCaptor.getValue();
+        assertEquals(expectedTransformedMappingString, transformedMapping);
+    }
+
+    public void testApplyTransformers_WhenTransformerFails_ShouldCallOnFailure() {
+        final String inputMappingString = "{\"field\": \"value\"}";
+        final String errorMsg = "Transformation failed";
+
+        doAnswer(invocation -> {
+            ActionListener<Void> actionListener = invocation.getArgument(2);
+            actionListener.onFailure(new RuntimeException(errorMsg));
+            return null;
+        }).when(transformer).transform(any(), any(), any());
+
+        registry.applyTransformers(inputMappingString, null, listener);
+
+        ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
+        verify(listener).onFailure(exceptionCaptor.capture());
+
+        assertTrue(exceptionCaptor.getValue() instanceof RuntimeException);
+        assertTrue(exceptionCaptor.getValue().getMessage().contains(errorMsg));
+    }
+}
diff --git a/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java b/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java
index 63758cb44dc5a..613c6dc92a2e2 100644
--- a/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java
+++ b/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java
@@ -53,7 +53,6 @@
 import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest;
 import org.opensearch.action.support.ActionFilters;
 import org.opensearch.action.support.DestructiveOperations;
-import org.opensearch.action.support.PlainActionFuture;
 import org.opensearch.action.support.clustermanager.ClusterManagerNodeRequest;
 import org.opensearch.action.support.clustermanager.TransportClusterManagerNodeAction;
 import org.opensearch.action.support.clustermanager.TransportClusterManagerNodeActionUtils;
@@ -97,6 +96,7 @@
 import org.opensearch.common.settings.Settings;
 import org.opensearch.common.settings.SettingsModule;
 import org.opensearch.common.util.concurrent.ThreadContext;
+import org.opensearch.core.action.ActionListener;
 import org.opensearch.core.action.ActionResponse;
 import org.opensearch.core.index.Index;
 import org.opensearch.core.xcontent.NamedXContentRegistry;
@@ -104,6 +104,7 @@
 import org.opensearch.env.TestEnvironment;
 import org.opensearch.index.IndexService;
 import org.opensearch.index.mapper.MapperService;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.index.shard.IndexEventListener;
 import org.opensearch.indices.DefaultRemoteStoreSettings;
 import org.opensearch.indices.IndicesService;
@@ -196,6 +197,8 @@ public ClusterStateChanges(NamedXContentRegistry xContentRegistry, ThreadPool th
         Environment environment = TestEnvironment.newEnvironment(SETTINGS);
         Transport transport = mock(Transport.class); // it's not used
 
+        MappingTransformerRegistry mappingTransformerRegistry = new MappingTransformerRegistry(List.of(), xContentRegistry);
+
         // mocks
         clusterService = mock(ClusterService.class);
         Metadata metadata = Metadata.builder().build();
@@ -372,7 +375,8 @@ public IndexMetadata upgradeIndexMetadata(IndexMetadata indexMetadata, Version m
             threadPool,
             createIndexService,
             actionFilters,
-            indexNameExpressionResolver
+            indexNameExpressionResolver,
+            mappingTransformerRegistry
         );
 
         repositoriesService = new RepositoriesService(
@@ -511,7 +515,17 @@ private <Request extends ClusterManagerNodeRequest<Request>, Response extends Ac
                     masterNodeAction,
                     request,
                     clusterState,
-                    new PlainActionFuture<>()
+                    new ActionListener<Response>() {
+                        @Override
+                        public void onResponse(Response response) {
+
+                        }
+
+                        @Override
+                        public void onFailure(Exception e) {
+                            throw new RuntimeException(e.getMessage(), e);
+                        }
+                    }
                 );
             } catch (Exception e) {
                 throw new RuntimeException(e);
diff --git a/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java
index 580b507292ea8..8f565d6d3abfb 100644
--- a/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java
+++ b/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java
@@ -187,6 +187,7 @@
 import org.opensearch.index.SegmentReplicationPressureService;
 import org.opensearch.index.SegmentReplicationStatsTracker;
 import org.opensearch.index.analysis.AnalysisRegistry;
+import org.opensearch.index.mapper.MappingTransformerRegistry;
 import org.opensearch.index.remote.RemoteStorePressureService;
 import org.opensearch.index.remote.RemoteStoreStatsTrackerFactory;
 import org.opensearch.index.seqno.GlobalCheckpointSyncAction;
@@ -1932,6 +1933,8 @@ private final class TestClusterNode {
             private Coordinator coordinator;
             private RemoteStoreNodeService remoteStoreNodeService;
 
+            private final MappingTransformerRegistry mappingTransformerRegistry;
+
             private Map<ActionType, TransportAction> actions = new HashMap<>();
 
             TestClusterNode(DiscoveryNode node) throws IOException {
@@ -2193,6 +2196,9 @@ public void onFailure(final Exception e) {
                     DefaultRemoteStoreSettings.INSTANCE,
                     null
                 );
+
+                mappingTransformerRegistry = new MappingTransformerRegistry(List.of(), namedXContentRegistry);
+
                 actions.put(
                     CreateIndexAction.INSTANCE,
                     new TransportCreateIndexAction(
@@ -2201,7 +2207,8 @@ public void onFailure(final Exception e) {
                         threadPool,
                         metadataCreateIndexService,
                         actionFilters,
-                        indexNameExpressionResolver
+                        indexNameExpressionResolver,
+                        mappingTransformerRegistry
                     )
                 );
                 final MetadataDeleteIndexService metadataDeleteIndexService = new MetadataDeleteIndexService(
@@ -2307,7 +2314,8 @@ public void onFailure(final Exception e) {
                         metadataMappingService,
                         actionFilters,
                         indexNameExpressionResolver,
-                        new RequestValidators<>(Collections.emptyList())
+                        new RequestValidators<>(Collections.emptyList()),
+                        mappingTransformerRegistry
                     )
                 );
                 actions.put(