This is an automated email from the ASF dual-hosted git repository.

roryqi pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new 8e5f701910 [Cherry-pick to branch-1.3] [#14428] fix(policy): Accept 
known policyType case-insensitively (#14520) (#14619)
8e5f701910 is described below

commit 8e5f7019100899aff339719a5c6fb71a64dd3093
Author: MaSai <[email protected]>
AuthorDate: Thu Nov 5 10:11:22 2026 +0800

    [Cherry-pick to branch-1.3] [#14428] fix(policy): Accept known policyType 
case-insensitively (#14520) (#14619)
    
    ## Purpose
    
    Cherry-pick of #14520 onto `branch-1.3`, replacing bot PR #14615 (closed
    with conflicts).
    
    ## Changes
    
    - Accept known `policyType` values case-insensitively via
    `PolicyContentTypeIdResolver`.
    - branch-1.3 adaptations (main has more built-in policy types):
    - Resolver / OpenAPI / docs only cover `ICEBERG_COMPACTION` and `CUSTOM`
    - Drop `validateCreatePolicyType` that depended on main-only built-in
    types
      - OpenAPI `policies.yaml` adapted to 1.3 schema (no conflict markers)
    
    ## Closes
    
    Supersedes #14615
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../java/org/apache/gravitino/policy/Policy.java   |   3 +
 .../dto/requests/PolicyContentTypeIdResolver.java  |  91 +++++++++++
 .../dto/requests/PolicyCreateRequest.java          |  23 +--
 .../dto/requests/PolicyUpdateRequest.java          |  28 ++--
 .../dto/requests/TestPolicyCreateRequest.java      | 121 ++++++++++++++-
 .../dto/requests/TestPolicyUpdatesRequest.java     |  75 ++++++++-
 docs/manage-policies-in-gravitino.md               |   5 +
 docs/open-api/policies.yaml                        | 167 +++++++++++++++++++--
 .../server/web/rest/PolicyOperations.java          |  12 --
 .../server/web/rest/TestPolicyOperations.java      | 145 ++++++++++++++++++
 10 files changed, 619 insertions(+), 51 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/policy/Policy.java 
b/api/src/main/java/org/apache/gravitino/policy/Policy.java
index 7b27b01fcd..c38ef4c9ac 100644
--- a/api/src/main/java/org/apache/gravitino/policy/Policy.java
+++ b/api/src/main/java/org/apache/gravitino/policy/Policy.java
@@ -60,6 +60,9 @@ public interface Policy extends Auditable {
     /**
      * Get the built-in policy type from the policy type string.
      *
+     * <p>Matching is case-insensitive. Callers should persist and return 
{@link #policyType()} so
+     * responses use the canonical lowercase wire value.
+     *
      * @param policyType the policy type string
      * @return the matched built-in policy type
      * @throws IllegalArgumentException if the policy type is unknown or blank
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyContentTypeIdResolver.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyContentTypeIdResolver.java
new file mode 100644
index 0000000000..4e84bddd5f
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyContentTypeIdResolver.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.DatabindContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.dto.policy.PolicyContentDTO;
+import org.apache.gravitino.policy.Policy;
+
+/**
+ * Selects the policy content DTO from the sibling {@code policyType}, 
matching known types
+ * case-insensitively.
+ *
+ * <p>An unknown type resolves to no content class, so the content is bound as 
{@code null} and
+ * request validation reports the type by name.
+ */
+public class PolicyContentTypeIdResolver extends TypeIdResolverBase {
+
+  private static final Map<Policy.BuiltInType, Class<? extends 
PolicyContentDTO>> CONTENT_CLASSES =
+      ImmutableMap.<Policy.BuiltInType, Class<? extends 
PolicyContentDTO>>builder()
+          .put(
+              Policy.BuiltInType.ICEBERG_COMPACTION,
+              PolicyContentDTO.IcebergCompactionContentDTO.class)
+          .put(Policy.BuiltInType.CUSTOM, 
PolicyContentDTO.CustomContentDTO.class)
+          .build();
+
+  private JavaType baseType;
+
+  @Override
+  public void init(JavaType baseType) {
+    this.baseType = baseType;
+  }
+
+  @Override
+  public String idFromValue(Object value) {
+    return idFromValueAndType(value, value.getClass());
+  }
+
+  @Override
+  public String idFromValueAndType(Object value, Class<?> suggestedType) {
+    if (suggestedType == Void.class) {
+      // Jackson asks for the id of the Void defaultImpl when policyType is 
absent. A null id fails
+      // deserialization, so return one that resolves to no content class: the 
content then binds
+      // as null and request validation reports the missing policyType.
+      return "";
+    }
+    for (Map.Entry<Policy.BuiltInType, Class<? extends PolicyContentDTO>> 
entry :
+        CONTENT_CLASSES.entrySet()) {
+      if (entry.getValue().isAssignableFrom(suggestedType)) {
+        return entry.getKey().policyType();
+      }
+    }
+    throw new IllegalArgumentException("Unknown policy content class: " + 
suggestedType);
+  }
+
+  @Override
+  public JavaType typeFromId(DatabindContext context, String id) {
+    for (Map.Entry<Policy.BuiltInType, Class<? extends PolicyContentDTO>> 
entry :
+        CONTENT_CLASSES.entrySet()) {
+      if (entry.getKey().policyType().equalsIgnoreCase(id)) {
+        return context.constructSpecializedType(baseType, entry.getValue());
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public JsonTypeInfo.Id getMechanism() {
+    return JsonTypeInfo.Id.NAME;
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
index 158bbb3e43..bf2e774d9c 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
@@ -19,8 +19,8 @@
 package org.apache.gravitino.dto.requests;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
 import com.google.common.base.Preconditions;
 import javax.annotation.Nullable;
 import lombok.EqualsAndHashCode;
@@ -28,9 +28,15 @@ import lombok.Getter;
 import lombok.ToString;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.dto.policy.PolicyContentDTO;
+import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.rest.RESTRequest;
 
-/** Represents a request to create a policy. */
+/**
+ * Represents a request to create a policy.
+ *
+ * <p>{@code policyType} accepts known type ids case-insensitively; storage 
and responses use the
+ * canonical lowercase value from {@link Policy.BuiltInType#policyType()}.
+ */
 @Getter
 @EqualsAndHashCode
 @ToString
@@ -54,15 +60,8 @@ public class PolicyCreateRequest implements RESTRequest {
       use = JsonTypeInfo.Id.NAME,
       include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
       property = "policyType",
-      defaultImpl = PolicyContentDTO.CustomContentDTO.class)
-  @JsonSubTypes({
-    // add mappings for built-in types here
-    // For example: @JsonSubTypes.Type(value = DataCompactionContent.class, 
name =
-    // "system_data_compaction")
-    @JsonSubTypes.Type(
-        value = PolicyContentDTO.IcebergCompactionContentDTO.class,
-        name = "system_iceberg_compaction")
-  })
+      defaultImpl = Void.class)
+  @JsonTypeIdResolver(PolicyContentTypeIdResolver.class)
   private final PolicyContentDTO policyContent;
 
   /**
@@ -99,6 +98,8 @@ public class PolicyCreateRequest implements RESTRequest {
         StringUtils.isNotBlank(name), "\"name\" is required and cannot be 
empty");
     Preconditions.checkArgument(
         StringUtils.isNotBlank(policyType), "\"policyType\" is required and 
cannot be empty");
+    // An unknown policyType binds content as null, so resolve the type before 
the content checks.
+    Policy.BuiltInType.fromPolicyType(policyType);
     Preconditions.checkArgument(
         policyContent != null, "\"content\" is required and cannot be null");
     policyContent.validate();
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
index 2cc53c68d0..59bf7265bf 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
@@ -24,12 +24,14 @@ import 
com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
 import com.google.common.base.Preconditions;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.ToString;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.dto.policy.PolicyContentDTO;
+import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.policy.PolicyChange;
 import org.apache.gravitino.rest.RESTRequest;
 
@@ -121,7 +123,15 @@ public interface PolicyUpdateRequest extends RESTRequest {
     public void validate() throws IllegalArgumentException {}
   }
 
-  /** The policy update request for updating a policy content. */
+  /**
+   * The policy update request for updating a policy content.
+   *
+   * <p>{@code newContent} fully replaces the previous content. Omitted typed 
fields use that
+   * content type's defaults; they are not merged with the previously stored 
values.
+   *
+   * <p>{@code policyType} accepts known type ids case-insensitively; the 
change stores the
+   * canonical lowercase value from {@link Policy.BuiltInType#policyType()}.
+   */
   @EqualsAndHashCode
   @Getter
   @ToString
@@ -135,15 +145,8 @@ public interface PolicyUpdateRequest extends RESTRequest {
         use = JsonTypeInfo.Id.NAME,
         include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
         property = "policyType",
-        defaultImpl = PolicyContentDTO.CustomContentDTO.class)
-    @JsonSubTypes({
-      // add mappings for built-in types here
-      // For example: @JsonSubTypes.Type(value = DataCompactionContent.class, 
name =
-      // "system_data_compaction")
-      @JsonSubTypes.Type(
-          value = PolicyContentDTO.IcebergCompactionContentDTO.class,
-          name = "system_iceberg_compaction")
-    })
+        defaultImpl = Void.class)
+    @JsonTypeIdResolver(PolicyContentTypeIdResolver.class)
     private final PolicyContentDTO newContent;
 
     /**
@@ -165,12 +168,15 @@ public interface PolicyUpdateRequest extends RESTRequest {
 
     @Override
     public PolicyChange policyChange() {
-      return PolicyChange.updateContent(policyType, fromDTO(newContent));
+      return PolicyChange.updateContent(
+          Policy.BuiltInType.fromPolicyType(policyType).policyType(), 
fromDTO(newContent));
     }
 
     @Override
     public void validate() throws IllegalArgumentException {
       Preconditions.checkArgument(policyType != null, "\"policyType\" must not 
be null");
+      // An unknown policyType binds newContent as null, so resolve the type 
first.
+      Policy.BuiltInType.fromPolicyType(policyType);
       Preconditions.checkArgument(newContent != null, "\"newContent\" must not 
be null");
       newContent.validate();
     }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyCreateRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyCreateRequest.java
index 81b33ca3de..6ececfaae1 100644
--- 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyCreateRequest.java
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyCreateRequest.java
@@ -24,8 +24,11 @@ import com.google.common.collect.ImmutableSet;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.dto.policy.PolicyContentDTO;
 import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.policy.Policy;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 public class TestPolicyCreateRequest {
 
@@ -39,14 +42,14 @@ public class TestPolicyCreateRequest {
             .withProperties(null)
             .build();
     PolicyCreateRequest request =
-        new PolicyCreateRequest("policy_test", "test_type", "policy comment", 
true, content);
+        new PolicyCreateRequest("policy_test", "custom", "policy comment", 
true, content);
     String serJson = JsonUtils.objectMapper().writeValueAsString(request);
     PolicyCreateRequest deserRequest =
         JsonUtils.objectMapper().readValue(serJson, PolicyCreateRequest.class);
     Assertions.assertEquals(request, deserRequest);
     Assertions.assertEquals("policy_test", deserRequest.getName());
     Assertions.assertEquals("policy comment", deserRequest.getComment());
-    Assertions.assertEquals("test_type", deserRequest.getPolicyType());
+    Assertions.assertEquals("custom", deserRequest.getPolicyType());
     Assertions.assertTrue(deserRequest.getEnabled());
     Assertions.assertEquals(content, deserRequest.getPolicyContent());
 
@@ -59,17 +62,127 @@ public class TestPolicyCreateRequest {
             .build();
     request =
         new PolicyCreateRequest(
-            "policy_test_null_types", "test_type", "policy comment", true, 
contentWithNullTypes);
+            "policy_test_null_types", "custom", "policy comment", true, 
contentWithNullTypes);
     serJson = JsonUtils.objectMapper().writeValueAsString(request);
     deserRequest = JsonUtils.objectMapper().readValue(serJson, 
PolicyCreateRequest.class);
     Assertions.assertEquals(request, deserRequest);
     Assertions.assertEquals("policy_test_null_types", deserRequest.getName());
     Assertions.assertEquals("policy comment", deserRequest.getComment());
-    Assertions.assertEquals("test_type", deserRequest.getPolicyType());
+    Assertions.assertEquals("custom", deserRequest.getPolicyType());
     Assertions.assertTrue(deserRequest.getEnabled());
     Assertions.assertEquals(contentWithNullTypes, 
deserRequest.getPolicyContent());
 
     Exception e = Assertions.assertThrows(IllegalArgumentException.class, 
request::validate);
     Assertions.assertEquals("supportedObjectTypes cannot be empty", 
e.getMessage());
   }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "system_iceberg_compaction",
+        "SYSTEM_ICEBERG_COMPACTION",
+        "System_Iceberg_Compaction"
+      })
+  public void testDeserializeAcceptsKnownPolicyTypeCaseInsensitively(String 
policyType)
+      throws JsonProcessingException {
+    String json =
+        "{\"name\":\"p_upper\",\"policyType\":\""
+            + policyType
+            + "\",\"enabled\":true,\"content\":{\"maxPartitionNum\":5}}";
+    PolicyCreateRequest request =
+        JsonUtils.objectMapper().readValue(json, PolicyCreateRequest.class);
+    Assertions.assertEquals(policyType, request.getPolicyType());
+    Assertions.assertInstanceOf(
+        PolicyContentDTO.IcebergCompactionContentDTO.class, 
request.getPolicyContent());
+    Assertions.assertEquals(
+        Policy.BuiltInType.ICEBERG_COMPACTION,
+        Policy.BuiltInType.fromPolicyType(request.getPolicyType()));
+    request.validate();
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"custom", "CUSTOM", "Custom"})
+  public void testDeserializeAcceptsCustomPolicyTypeCaseInsensitively(String 
policyType)
+      throws JsonProcessingException {
+    String json =
+        "{\"name\":\"p\",\"policyType\":\""
+            + policyType
+            + "\",\"enabled\":true,"
+            + 
"\"content\":{\"customRules\":{\"a\":1},\"supportedObjectTypes\":[\"TABLE\"]}}";
+    PolicyCreateRequest request =
+        JsonUtils.objectMapper().readValue(json, PolicyCreateRequest.class);
+    Assertions.assertEquals(
+        Policy.BuiltInType.CUSTOM, 
Policy.BuiltInType.fromPolicyType(request.getPolicyType()));
+    request.validate();
+  }
+
+  @Test
+  public void testDeserializeRejectsUnknownPolicyType() throws 
JsonProcessingException {
+    assertPolicyTypeNamedOnDeserialize(
+        
"{\"name\":\"p_bogus\",\"policyType\":\"system_iceberg_bogus\",\"enabled\":true,\"content\":{}}",
+        "Unknown built-in policy type: system_iceberg_bogus");
+    assertPolicyTypeNamedOnDeserialize(
+        
"{\"name\":\"p_bogus\",\"policyType\":\"system_iceberg_bogus\",\"enabled\":true,"
+            + "\"content\":{\"maxPartitionNum\":5}}",
+        "Unknown built-in policy type: system_iceberg_bogus");
+    assertPolicyTypeNamedOnDeserialize(
+        
"{\"name\":\"p_other\",\"policyType\":\"not_a_policy\",\"enabled\":true,"
+            + 
"\"content\":{\"customRules\":{},\"supportedObjectTypes\":[\"TABLE\"]}}",
+        "Unknown policy type: not_a_policy, it should start with 'system_' or 
be 'custom'");
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {"{}", 
"{\"customRules\":{},\"supportedObjectTypes\":[\"TABLE\"]}", "null"})
+  public void testDeserializeReportsMissingPolicyType(String content)
+      throws JsonProcessingException {
+    assertPolicyTypeNamedOnDeserialize(
+        "{\"name\":\"p\",\"content\":" + content + "}",
+        "\"policyType\" is required and cannot be empty");
+  }
+
+  @Test
+  public void testDeserializeRejectsMalformedEnabled() {
+    String content = 
"\"content\":{\"customRules\":{},\"supportedObjectTypes\":[\"table\"]}}";
+    for (String enabled : new String[] {"\"typo\"", "{}", "[]"}) {
+      String json =
+          "{\"name\":\"p\",\"policyType\":\"custom\",\"enabled\":" + enabled + 
"," + content;
+      Assertions.assertThrows(
+          JsonProcessingException.class,
+          () -> JsonUtils.objectMapper().readValue(json, 
PolicyCreateRequest.class),
+          enabled);
+    }
+  }
+
+  @Test
+  public void testDeserializeRejectsUnknownProperty() {
+    String json =
+        "{\"name\":\"p\",\"policyType\":\"custom\",\"enabld\":false,"
+            + 
"\"content\":{\"customRules\":{},\"supportedObjectTypes\":[\"table\"]}}";
+    JsonProcessingException thrown =
+        Assertions.assertThrows(
+            JsonProcessingException.class,
+            () -> JsonUtils.objectMapper().readValue(json, 
PolicyCreateRequest.class));
+    Assertions.assertTrue(thrown.getMessage().contains("enabld"), 
thrown.getMessage());
+  }
+
+  @Test
+  public void testDeserializeEnabledFalse() throws JsonProcessingException {
+    String json =
+        "{\"name\":\"p\",\"policyType\":\"custom\",\"enabled\":false,"
+            + 
"\"content\":{\"customRules\":{},\"supportedObjectTypes\":[\"table\"]}}";
+    PolicyCreateRequest request =
+        JsonUtils.objectMapper().readValue(json, PolicyCreateRequest.class);
+    Assertions.assertFalse(request.getEnabled());
+  }
+
+  private static void assertPolicyTypeNamedOnDeserialize(String json, String 
expectedMessage)
+      throws JsonProcessingException {
+    PolicyCreateRequest request =
+        JsonUtils.objectMapper().readValue(json, PolicyCreateRequest.class);
+    Assertions.assertNull(request.getPolicyContent());
+    IllegalArgumentException thrown =
+        Assertions.assertThrows(IllegalArgumentException.class, 
request::validate);
+    Assertions.assertEquals(expectedMessage, thrown.getMessage());
+  }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyUpdatesRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyUpdatesRequest.java
index 98da580afe..52c88b569b 100644
--- 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyUpdatesRequest.java
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestPolicyUpdatesRequest.java
@@ -21,11 +21,16 @@ package org.apache.gravitino.dto.requests;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import java.util.List;
+import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.dto.policy.PolicyContentDTO;
 import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.policy.Policy;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 public class TestPolicyUpdatesRequest {
 
@@ -57,19 +62,82 @@ public class TestPolicyUpdatesRequest {
     PolicyContentDTO contentDTO =
         PolicyContentDTO.CustomContentDTO.builder()
             .withCustomRules(ImmutableMap.of("rule1", "value1"))
+            
.withSupportedObjectTypes(ImmutableSet.of(MetadataObject.Type.TABLE))
             .withProperties(ImmutableMap.of("key", "value"))
             .build();
     PolicyUpdateRequest.UpdatePolicyContentRequest request =
-        new PolicyUpdateRequest.UpdatePolicyContentRequest("test_type", 
contentDTO);
+        new PolicyUpdateRequest.UpdatePolicyContentRequest("custom", 
contentDTO);
     String serJson = JsonUtils.objectMapper().writeValueAsString(request);
     PolicyUpdateRequest.UpdatePolicyContentRequest deserRequest =
         JsonUtils.objectMapper()
             .readValue(serJson, 
PolicyUpdateRequest.UpdatePolicyContentRequest.class);
     Assertions.assertEquals(request, deserRequest);
-    Assertions.assertEquals("test_type", deserRequest.getPolicyType());
+    Assertions.assertEquals("custom", deserRequest.getPolicyType());
     Assertions.assertEquals(contentDTO, deserRequest.getNewContent());
   }
 
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "system_iceberg_compaction",
+        "SYSTEM_ICEBERG_COMPACTION",
+        "System_Iceberg_Compaction"
+      })
+  public void 
testUpdatePolicyContentAcceptsKnownPolicyTypeCaseInsensitively(String 
policyType)
+      throws JsonProcessingException {
+    String json =
+        "{\"@type\":\"updateContent\",\"policyType\":\""
+            + policyType
+            + "\",\"newContent\":{\"maxPartitionNum\":5}}";
+    PolicyUpdateRequest.UpdatePolicyContentRequest request =
+        JsonUtils.objectMapper()
+            .readValue(json, 
PolicyUpdateRequest.UpdatePolicyContentRequest.class);
+    Assertions.assertEquals(policyType, request.getPolicyType());
+    Assertions.assertInstanceOf(
+        PolicyContentDTO.IcebergCompactionContentDTO.class, 
request.getNewContent());
+    request.validate();
+    Assertions.assertTrue(
+        request
+            .policyChange()
+            .toString()
+            .contains(Policy.BuiltInType.ICEBERG_COMPACTION.policyType()),
+        request.policyChange().toString());
+  }
+
+  @Test
+  public void testUpdatePolicyContentDeserializeRejectsUnknownPolicyType()
+      throws JsonProcessingException {
+    assertPolicyTypeNamedOnDeserialize(
+        "{\"@type\":\"updateContent\",\"policyType\":\"system_iceberg_bogus\","
+            + "\"newContent\":{}}",
+        "Unknown built-in policy type: system_iceberg_bogus");
+    assertPolicyTypeNamedOnDeserialize(
+        "{\"@type\":\"updateContent\",\"policyType\":\"system_iceberg_bogus\","
+            + "\"newContent\":{\"maxPartitionNum\":5}}",
+        "Unknown built-in policy type: system_iceberg_bogus");
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {"{}", 
"{\"customRules\":{},\"supportedObjectTypes\":[\"TABLE\"]}", "null"})
+  public void 
testUpdatePolicyContentDeserializeReportsMissingPolicyType(String content)
+      throws JsonProcessingException {
+    assertPolicyTypeNamedOnDeserialize(
+        "{\"@type\":\"updateContent\",\"newContent\":" + content + "}",
+        "\"policyType\" must not be null");
+  }
+
+  private static void assertPolicyTypeNamedOnDeserialize(String json, String 
expectedMessage)
+      throws JsonProcessingException {
+    PolicyUpdateRequest.UpdatePolicyContentRequest request =
+        JsonUtils.objectMapper()
+            .readValue(json, 
PolicyUpdateRequest.UpdatePolicyContentRequest.class);
+    Assertions.assertNull(request.getNewContent());
+    IllegalArgumentException thrown =
+        Assertions.assertThrows(IllegalArgumentException.class, 
request::validate);
+    Assertions.assertEquals(expectedMessage, thrown.getMessage());
+  }
+
   @Test
   public void testPolicyUpdatesRequestSerDe() throws JsonProcessingException {
     PolicyUpdateRequest request = new 
PolicyUpdateRequest.RenamePolicyRequest("policy_test_new");
@@ -77,9 +145,10 @@ public class TestPolicyUpdatesRequest {
         new PolicyUpdateRequest.UpdatePolicyCommentRequest("policy comment 
new");
     PolicyUpdateRequest request2 =
         new PolicyUpdateRequest.UpdatePolicyContentRequest(
-            "test_type",
+            "custom",
             PolicyContentDTO.CustomContentDTO.builder()
                 .withCustomRules(ImmutableMap.of("rule1", "value1"))
+                
.withSupportedObjectTypes(ImmutableSet.of(MetadataObject.Type.TABLE))
                 .withProperties(ImmutableMap.of("key", "value"))
                 .build());
 
diff --git a/docs/manage-policies-in-gravitino.md 
b/docs/manage-policies-in-gravitino.md
index f1edbba297..3c1ab3bfd9 100644
--- a/docs/manage-policies-in-gravitino.md
+++ b/docs/manage-policies-in-gravitino.md
@@ -23,6 +23,11 @@ The Python client does not cover policies, so the examples 
below are REST and Ja
 A policy needs a name and a type. Content carries the rules, the object types 
the policy supports,
 and optional properties. `supportedObjectTypes` cannot be changed after 
creation.
 
+`policyType` accepts the known wire values case-insensitively (for example 
`CUSTOM` and
+`System_Iceberg_Compaction`). Gravitino stores and returns the canonical 
lowercase value listed in
+the OpenAPI schema. Unknown types such as `system_iceberg_bogus` return HTTP 
400 that names the
+type.
+
 <Tabs groupId='language' queryString>
 <TabItem value="shell" label="REST">
 
diff --git a/docs/open-api/policies.yaml b/docs/open-api/policies.yaml
index 7513b3bb72..56ffd9fb8e 100644
--- a/docs/open-api/policies.yaml
+++ b/docs/open-api/policies.yaml
@@ -70,6 +70,12 @@ paths:
             examples:
               PolicyCreate:
                 $ref: "#/components/examples/PolicyCreate"
+              PolicyCreateLowercaseType:
+                $ref: "#/components/examples/PolicyCreateLowercaseType"
+              PolicyCreateUppercaseType:
+                $ref: "#/components/examples/PolicyCreateUppercaseType"
+              PolicyCreateMixedCaseType:
+                $ref: "#/components/examples/PolicyCreateMixedCaseType"
 
       responses:
         "200":
@@ -139,6 +145,13 @@ paths:
           application/json:
             schema:
               $ref: "#/components/schemas/PolicyUpdatesRequest"
+            examples:
+              PolicyUpdateContentLowercaseType:
+                $ref: "#/components/examples/PolicyUpdateContentLowercaseType"
+              PolicyUpdateContentUppercaseType:
+                $ref: "#/components/examples/PolicyUpdateContentUppercaseType"
+              PolicyUpdateContentMixedCaseType:
+                $ref: "#/components/examples/PolicyUpdateContentMixedCaseType"
       responses:
         "200":
             description: Returns the updated policy object
@@ -538,6 +551,46 @@ components:
             - "FUNCTION"
             - "COLUMN"
 
+    CustomRequestPolicyType:
+      type: string
+      description: Request value `custom`, matched case-insensitively.
+      pattern: '^[cC][uU][sS][tT][oO][mM]$'
+
+    IcebergCompactionRequestPolicyType:
+      type: string
+      description: Request value `system_iceberg_compaction`, matched 
case-insensitively.
+      pattern: 
'^[sS][yY][sS][tT][eE][mM]_[iI][cC][eE][bB][eE][rR][gG]_[cC][oO][mM][pP][aA][cC][tT][iI][oO][nN]$'
+
+    IcebergCompactionContent:
+      type: object
+      description: Built-in Iceberg compaction thresholds, scoring weights, 
and rewrite options.
+      properties:
+        minDataFileMse:
+          type: integer
+          format: int64
+          minimum: 0
+        minDeleteFileNumber:
+          type: integer
+          format: int64
+          minimum: 0
+        dataFileMseWeight:
+          type: integer
+          format: int64
+          minimum: 0
+        deleteFileNumberWeight:
+          type: integer
+          format: int64
+          minimum: 0
+        maxPartitionNum:
+          type: integer
+          format: int64
+          minimum: 1
+        rewriteOptions:
+          type: object
+          description: Rewrite options expanded to job.options.* during rule 
generation.
+          additionalProperties:
+            type: string
+
     PolicyCreateRequestBase:
       type: object
       description: A base policy create request without content
@@ -568,18 +621,40 @@ components:
           required:
             - content
           properties:
+            policyType:
+              $ref: '#/components/schemas/CustomRequestPolicyType'
             content:
               $ref: '#/components/schemas/CustomPolicyContent'
 
+    IcebergCompactionPolicyCreateRequest:
+      description: A request to create an Iceberg compaction policy.
+      allOf:
+        - $ref: '#/components/schemas/PolicyCreateRequestBase'
+        - type: object
+          required:
+            - content
+          properties:
+            policyType:
+              $ref: '#/components/schemas/IcebergCompactionRequestPolicyType'
+            content:
+              $ref: '#/components/schemas/IcebergCompactionContent'
+
     PolicyCreateRequest:
-      description: A request to create a policy.
+      description: >-
+        A request to create a policy. Known `policyType` values are matched 
case-insensitively;
+        the server stores and returns the canonical lowercase form.
       type: object
       oneOf:
         - $ref: '#/components/schemas/CustomPolicyCreateRequest'
-      discriminator:
-        propertyName: policyType
-        mapping:
-          custom: '#/components/schemas/CustomPolicyCreateRequest'
+        - $ref: '#/components/schemas/IcebergCompactionPolicyCreateRequest'
+      example: {
+        "name": "compaction_upper",
+        "policyType": "SYSTEM_ICEBERG_COMPACTION",
+        "enabled": true,
+        "content": {
+          "maxPartitionNum": 5
+        }
+      }
 
 
     PolicyUpdatesRequest:
@@ -660,16 +735,16 @@ components:
             - updateContent
         policyType:
           type: string
-          description: The type of the policy
+          description: >-
+            The type of the policy, either `custom` or 
`system_iceberg_compaction`. Known
+            values are matched case-insensitively; the server stores and 
returns the canonical
+            lowercase form. An unknown value is rejected with HTTP 400 naming 
the type.
         newContent:
           type: object
           description: The new content of the policy, its structure depends on 
the 'policyType'.
           oneOf:
               - $ref: '#/components/schemas/CustomPolicyContent'
-          discriminator:
-            propertyName: policyType
-            mapping:
-              custom: '#/components/schemas/CustomPolicyContent'
+              - $ref: '#/components/schemas/IcebergCompactionContent'
       example: {
         "@type": "updateContent",
         "policyType": "custom",
@@ -814,6 +889,78 @@ components:
         }
       }
 
+    PolicyCreateLowercaseType:
+      summary: Create a compaction policy with the canonical lowercase 
policyType
+      value: {
+        "name": "compaction_policy",
+        "policyType": "system_iceberg_compaction",
+        "content": {
+          "maxPartitionNum": 5
+        }
+      }
+
+    PolicyCreateUppercaseType:
+      summary: Create a compaction policy with an uppercase policyType
+      value: {
+        "name": "compaction_policy",
+        "policyType": "SYSTEM_ICEBERG_COMPACTION",
+        "content": {
+          "maxPartitionNum": 5
+        }
+      }
+
+    PolicyCreateMixedCaseType:
+      summary: Create a compaction policy with a mixed-case policyType
+      value: {
+        "name": "compaction_policy",
+        "policyType": "System_Iceberg_Compaction",
+        "content": {
+          "maxPartitionNum": 5
+        }
+      }
+
+    PolicyUpdateContentLowercaseType:
+      summary: Update compaction content with the canonical lowercase 
policyType
+      value: {
+        "updates": [
+          {
+            "@type": "updateContent",
+            "policyType": "system_iceberg_compaction",
+            "newContent": {
+              "maxPartitionNum": 5
+            }
+          }
+        ]
+      }
+
+    PolicyUpdateContentUppercaseType:
+      summary: Update compaction content with an uppercase policyType
+      value: {
+        "updates": [
+          {
+            "@type": "updateContent",
+            "policyType": "SYSTEM_ICEBERG_COMPACTION",
+            "newContent": {
+              "maxPartitionNum": 5
+            }
+          }
+        ]
+      }
+
+    PolicyUpdateContentMixedCaseType:
+      summary: Update compaction content with a mixed-case policyType
+      value: {
+        "updates": [
+          {
+            "@type": "updateContent",
+            "policyType": "System_Iceberg_Compaction",
+            "newContent": {
+              "maxPartitionNum": 5
+            }
+          }
+        ]
+      }
+
     PolicyAssociate:
       value: {
         "policiesToAdd": ["my_policy1", "my_policy2"],
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
index c1ad6c2766..a9b63111e1 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/PolicyOperations.java
@@ -160,7 +160,6 @@ public class PolicyOperations {
             request.validate();
             NameIdentifierUtil.checkMetadataObjectName(
                 NameIdentifierUtil.ofPolicy(metalake, request.getName()), 
Entity.EntityType.POLICY);
-            validateCreatePolicyType(request.getPolicyType());
             PolicyEntity policy =
                 policyDispatcher.createPolicy(
                     metalake,
@@ -379,15 +378,4 @@ public class PolicyOperations {
 
     return builder.build();
   }
-
-  private static void validateCreatePolicyType(String policyType) {
-    Policy.BuiltInType builtInType = 
Policy.BuiltInType.fromPolicyType(policyType);
-    if (builtInType != Policy.BuiltInType.CUSTOM
-        && !builtInType.policyType().equalsIgnoreCase(policyType)) {
-      throw new IllegalArgumentException(
-          String.format(
-              "Built-in policy type must use prefixed value '%s', but got: %s",
-              builtInType.policyType(), policyType));
-    }
-  }
 }
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
index 91232fc7a9..673ffb3eef 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPolicyOperations.java
@@ -77,6 +77,8 @@ import org.glassfish.jersey.test.TestProperties;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 
 public class TestPolicyOperations extends BaseOperationsTest {
@@ -446,6 +448,75 @@ public class TestPolicyOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp.getStatus());
   }
 
+  @Test
+  public void testCreatePolicyUnknownTypeNamedInError() {
+    assertCreatePolicyTypeNamedInError(
+        
"{\"name\":\"p_bogus\",\"comment\":\"\",\"policyType\":\"system_iceberg_bogus\","
+            + "\"enabled\":true,\"content\":{}}",
+        "Unknown built-in policy type: system_iceberg_bogus");
+    assertCreatePolicyTypeNamedInError(
+        
"{\"name\":\"p_bogus\",\"comment\":\"\",\"policyType\":\"system_iceberg_bogus\","
+            + "\"enabled\":true,\"content\":{\"maxPartitionNum\":5}}",
+        "Unknown built-in policy type: system_iceberg_bogus");
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {"{}", 
"{\"customRules\":{},\"supportedObjectTypes\":[\"TABLE\"]}", "null"})
+  public void testCreatePolicyMissingTypeNamedInError(String content) {
+    assertCreatePolicyTypeNamedInError(
+        "{\"name\":\"p\",\"content\":" + content + "}",
+        "\\\"policyType\\\" is required and cannot be empty");
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"SYSTEM_ICEBERG_COMPACTION", 
"System_Iceberg_Compaction"})
+  public void testCreatePolicyAcceptsMisCasedKnownType(String policyType) {
+    PolicyContent content = PolicyContents.icebergDataCompaction();
+    PolicyEntity policy =
+        PolicyEntity.builder()
+            .withId(1L)
+            .withName("p_upper")
+            .withPolicyType(Policy.BuiltInType.ICEBERG_COMPACTION)
+            .withEnabled(true)
+            .withContent(content)
+            .withAuditInfo(testAuditInfo1)
+            .build();
+    when(policyManager.createPolicy(any(), any(), any(), any(), anyBoolean(), 
any()))
+        .thenAnswer(
+            invocation -> {
+              Assertions.assertEquals(
+                  Policy.BuiltInType.ICEBERG_COMPACTION, 
invocation.getArgument(2));
+              return policy;
+            });
+
+    Response resp =
+        target(policyPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(
+                Entity.entity(
+                    "{\"name\":\"p_upper\",\"comment\":\"\",\"policyType\":\""
+                        + policyType
+                        + 
"\",\"enabled\":true,\"content\":{\"maxPartitionNum\":5}}",
+                    MediaType.APPLICATION_JSON_TYPE));
+    Assertions.assertEquals(
+        Response.Status.OK.getStatusCode(), resp.getStatus(), 
resp.readEntity(String.class));
+  }
+
+  private void assertCreatePolicyTypeNamedInError(String body, String 
expectedMessage) {
+    Response resp =
+        target(policyPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(body, MediaType.APPLICATION_JSON_TYPE));
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp.getStatus());
+    String msg = resp.readEntity(String.class);
+    Assertions.assertTrue(msg.contains(expectedMessage), msg);
+    Assertions.assertFalse(msg.contains("supportedObjectTypes"), msg);
+    Assertions.assertFalse(msg.contains("Malformed json request"), msg);
+  }
+
   @Test
   public void testCreatePolicyRejectsDottedName() {
     PolicyContent content =
@@ -590,6 +661,80 @@ public class TestPolicyOperations extends 
BaseOperationsTest {
     }
   }
 
+  @ParameterizedTest
+  @ValueSource(strings = {"SYSTEM_ICEBERG_COMPACTION", 
"System_Iceberg_Compaction"})
+  public void testAlterPolicyContentAcceptsMisCasedKnownType(String 
policyType) {
+    PolicyContent content = PolicyContents.icebergDataCompaction();
+    PolicyEntity policy =
+        PolicyEntity.builder()
+            .withId(1L)
+            .withName("p_upper")
+            .withPolicyType(Policy.BuiltInType.ICEBERG_COMPACTION)
+            .withEnabled(true)
+            .withContent(content)
+            .withAuditInfo(testAuditInfo1)
+            .build();
+    when(policyManager.alterPolicy(any(), any(), any()))
+        .thenAnswer(
+            invocation -> {
+              PolicyChange.UpdateContent change = invocation.getArgument(2);
+              Assertions.assertEquals(
+                  Policy.BuiltInType.ICEBERG_COMPACTION.policyType(), 
change.getPolicyType());
+              return policy;
+            });
+
+    Response resp =
+        target(policyPath(metalake))
+            .path("p_upper")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(
+                Entity.entity(
+                    "{\"updates\":[{\"@type\":\"updateContent\","
+                        + "\"policyType\":\""
+                        + policyType
+                        + "\",\"newContent\":{\"maxPartitionNum\":5}}]}",
+                    MediaType.APPLICATION_JSON_TYPE));
+    Assertions.assertEquals(
+        Response.Status.OK.getStatusCode(), resp.getStatus(), 
resp.readEntity(String.class));
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {"{}", 
"{\"customRules\":{},\"supportedObjectTypes\":[\"TABLE\"]}", "null"})
+  public void testAlterPolicyContentMissingTypeNamedInError(String content) {
+    Response resp =
+        target(policyPath(metalake))
+            .path("p_missing")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(
+                Entity.entity(
+                    
"{\"updates\":[{\"@type\":\"updateContent\",\"newContent\":" + content + "}]}",
+                    MediaType.APPLICATION_JSON_TYPE));
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp.getStatus());
+    String msg = resp.readEntity(String.class);
+    Assertions.assertTrue(msg.contains("\\\"policyType\\\" must not be null"), 
msg);
+  }
+
+  @Test
+  public void testAlterPolicyContentUnknownTypeNamedInError() {
+    Response resp =
+        target(policyPath(metalake))
+            .path("p_bogus")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(
+                Entity.entity(
+                    "{\"updates\":[{\"@type\":\"updateContent\","
+                        + "\"policyType\":\"system_iceberg_bogus\","
+                        + "\"newContent\":{\"maxPartitionNum\":5}}]}",
+                    MediaType.APPLICATION_JSON_TYPE));
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp.getStatus());
+    String msg = resp.readEntity(String.class);
+    Assertions.assertTrue(msg.contains("Unknown built-in policy type: 
system_iceberg_bogus"), msg);
+  }
+
   @Test
   public void testAlterPolicy() {
     ImmutableMap<String, Object> contentFields = 
ImmutableMap.of("target_file_size_bytes", 1000);

Reply via email to