diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java
new file mode 100644
index 000000000..a1e82f7b6
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java
@@ -0,0 +1,22 @@
+/*
+ * 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.polaris.core.policy;
+
+/** A marker interface for policy content */
+public interface PolicyContent {}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/InvalidPolicyException.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/InvalidPolicyException.java
new file mode 100644
index 000000000..9672cfa61
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/InvalidPolicyException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import org.apache.polaris.core.exceptions.PolarisException;
+
+/** Exception thrown when a policy is invalid or violates defined rules. */
+public class InvalidPolicyException extends PolarisException {
+ public InvalidPolicyException(String message) {
+ super(message);
+ }
+
+ public InvalidPolicyException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public InvalidPolicyException(Throwable cause) {
+ super("Invalid policy", cause);
+ }
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidator.java
new file mode 100644
index 000000000..9abd097ed
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidator.java
@@ -0,0 +1,47 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import org.apache.polaris.core.entity.PolarisEntitySubType;
+import org.apache.polaris.core.entity.PolarisEntityType;
+
+/** Validates and parses a given policy content string against its defined schema. */
+public interface PolicyValidator {
+
+ /**
+ * Validates the provided policy content.
+ *
+ * @param content the policy content to parse and validate
+ * @throws InvalidPolicyException if the content is not valid
+ */
+ void validate(String content) throws InvalidPolicyException;
+
+ /**
+ * Determines whether the policy is attachable to a target entity.
+ *
+ *
This method examines the provided {@link PolarisEntityType} and {@link PolarisEntitySubType}
+ * to decide if a policy is applicable for attachment to the target entity.
+ *
+ * @param entityType the type of the target entity
+ * @param entitySubType the subtype of the target entity
+ * @return {@code true} if the policy can be attached to the target entity; {@code false}
+ * otherwise
+ */
+ boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType);
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java
new file mode 100644
index 000000000..a08cdfd7e
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java
@@ -0,0 +1,35 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class PolicyValidatorUtil {
+ public static final ObjectMapper MAPPER = configureMapper();
+
+ private static ObjectMapper configureMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+ // Fails if a required field (in the constructor) is missing
+ mapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true);
+ // Fails if a required field is present but explicitly null, e.g., {"enable": null}
+ mapper.configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true);
+ return mapper;
+ }
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java
new file mode 100644
index 000000000..ed37f1279
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java
@@ -0,0 +1,95 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import com.google.common.base.Preconditions;
+import org.apache.polaris.core.entity.PolarisEntity;
+import org.apache.polaris.core.policy.PolicyEntity;
+import org.apache.polaris.core.policy.PredefinedPolicyTypes;
+import org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyValidator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Validates a given {@link PolicyEntity} against its defined policy type.
+ *
+ *
This class maps the policy type code from the {@code PolicyEntity} to a predefined policy
+ * type, then delegates parsing/validation to a specific validator implementation.
+ */
+public class PolicyValidators {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PolicyValidators.class);
+
+ /**
+ * Validates the given policy.
+ *
+ * @param policy the policy entity to validate
+ * @throws InvalidPolicyException if the policy type is unknown or unsupported, or if the policy
+ * content is invalid
+ */
+ public static void validate(PolicyEntity policy) {
+ Preconditions.checkNotNull(policy, "Policy must not be null");
+
+ var type = PredefinedPolicyTypes.fromCode(policy.getPolicyTypeCode());
+ Preconditions.checkArgument(type != null, "Unknown policy type: " + policy.getPolicyTypeCode());
+
+ switch (type) {
+ case DATA_COMPACTION:
+ DataCompactionPolicyValidator.INSTANCE.validate(policy.getContent());
+ break;
+
+ // To support additional policy types in the future, add cases here.
+ case METADATA_COMPACTION:
+ case SNAPSHOT_RETENTION:
+ case ORPHAN_FILE_REMOVAL:
+ default:
+ throw new InvalidPolicyException("Unsupported policy type: " + type.getName());
+ }
+
+ LOGGER.info("Policy validated successfully: {}", type.getName());
+ }
+
+ /**
+ * Determines whether the given policy can be attached to the specified target entity.
+ *
+ * @param policy the policy entity to check
+ * @param targetEntity the target Polaris entity to attach the policy to
+ * @return {@code true} if the policy is attachable to the target entity; {@code false} otherwise
+ */
+ public static boolean canAttach(PolicyEntity policy, PolarisEntity targetEntity) {
+ Preconditions.checkNotNull(policy, "Policy must not be null");
+ Preconditions.checkNotNull(targetEntity, "Target entity must not be null");
+
+ var policyType = PredefinedPolicyTypes.fromCode(policy.getPolicyTypeCode());
+ Preconditions.checkArgument(
+ policyType != null, "Unknown policy type: " + policy.getPolicyTypeCode());
+
+ switch (policyType) {
+ case DATA_COMPACTION:
+ return DataCompactionPolicyValidator.INSTANCE.canAttach(
+ targetEntity.getType(), targetEntity.getSubType());
+ // To support additional policy types in the future, add cases here.
+ case METADATA_COMPACTION:
+ case SNAPSHOT_RETENTION:
+ case ORPHAN_FILE_REMOVAL:
+ default:
+ LOGGER.warn("Attachment not supported for policy type: {}", policyType.getName());
+ return false;
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java
new file mode 100644
index 000000000..f6da87e70
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java
@@ -0,0 +1,38 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import java.io.IOException;
+
+public class StrictBooleanDeserializer extends JsonDeserializer {
+ @Override
+ public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ String text = p.getText();
+ if ("true".equals(text)) {
+ return Boolean.TRUE;
+ } else if ("false".equals(text)) {
+ return Boolean.FALSE;
+ } else {
+ throw new InvalidPolicyException("Invalid boolean value: " + text);
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java
new file mode 100644
index 000000000..efdd158b5
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java
@@ -0,0 +1,97 @@
+/*
+ * 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.polaris.core.policy.validator.datacompaction;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.google.common.base.Strings;
+import java.util.Map;
+import java.util.Set;
+import org.apache.polaris.core.policy.PolicyContent;
+import org.apache.polaris.core.policy.validator.InvalidPolicyException;
+import org.apache.polaris.core.policy.validator.PolicyValidatorUtil;
+import org.apache.polaris.core.policy.validator.StrictBooleanDeserializer;
+
+public class DataCompactionPolicyContent implements PolicyContent {
+ private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03";
+ private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION);
+
+ @JsonDeserialize(using = StrictBooleanDeserializer.class)
+ private Boolean enable;
+
+ private String version;
+ private Map config;
+
+ @JsonCreator
+ public DataCompactionPolicyContent(
+ @JsonProperty(value = "enable", required = true) boolean enable) {
+ this.enable = enable;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public Boolean enabled() {
+ return enable;
+ }
+
+ public void setEnabled(Boolean enable) {
+ this.enable = enable;
+ }
+
+ public Map getConfig() {
+ return config;
+ }
+
+ public void setConfig(Map config) {
+ this.config = config;
+ }
+
+ public static DataCompactionPolicyContent fromString(String content) {
+ if (Strings.isNullOrEmpty(content)) {
+ throw new InvalidPolicyException("Policy is empty");
+ }
+
+ try {
+ DataCompactionPolicyContent policy =
+ PolicyValidatorUtil.MAPPER.readValue(content, DataCompactionPolicyContent.class);
+ if (policy == null) {
+ throw new InvalidPolicyException("Invalid policy");
+ }
+
+ if (Strings.isNullOrEmpty(policy.getVersion())) {
+ policy.setVersion(DEFAULT_POLICY_SCHEMA_VERSION);
+ }
+
+ if (!POLICY_SCHEMA_VERSIONS.contains(policy.getVersion())) {
+ throw new InvalidPolicyException("Invalid policy version: " + policy.getVersion());
+ }
+
+ return policy;
+ } catch (Exception e) {
+ throw new InvalidPolicyException(e);
+ }
+ }
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java
new file mode 100644
index 000000000..8ef8b4dcf
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java
@@ -0,0 +1,58 @@
+/*
+ * 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.polaris.core.policy.validator.datacompaction;
+
+import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG;
+import static org.apache.polaris.core.entity.PolarisEntityType.ICEBERG_TABLE_LIKE;
+import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE;
+
+import java.util.Set;
+import org.apache.polaris.core.entity.PolarisEntitySubType;
+import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.policy.validator.InvalidPolicyException;
+import org.apache.polaris.core.policy.validator.PolicyValidator;
+
+public class DataCompactionPolicyValidator implements PolicyValidator {
+ public static final DataCompactionPolicyValidator INSTANCE = new DataCompactionPolicyValidator();
+
+ private static final Set ATTACHABLE_ENTITY_TYPES =
+ Set.of(CATALOG, NAMESPACE, ICEBERG_TABLE_LIKE);
+
+ @Override
+ public void validate(String content) throws InvalidPolicyException {
+ DataCompactionPolicyContent.fromString(content);
+ }
+
+ @Override
+ public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType) {
+ if (entityType == null) {
+ return false;
+ }
+
+ if (!ATTACHABLE_ENTITY_TYPES.contains(entityType)) {
+ return false;
+ }
+
+ if (entityType == ICEBERG_TABLE_LIKE && entitySubType != PolarisEntitySubType.TABLE) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java
new file mode 100644
index 000000000..3d70fbeef
--- /dev/null
+++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import static org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyContent.fromString;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+public class DataCompactionPolicyContentTest {
+ @Test
+ public void testValidPolicies() {
+ assertThat(fromString("{\"enable\": false}").enabled()).isFalse();
+ assertThat(fromString("{\"enable\": true}").enabled()).isTrue();
+
+ var validJson = "{\"version\":\"2025-02-03\", \"enable\": true}";
+ assertThat(fromString(validJson).getVersion()).isEqualTo("2025-02-03");
+
+ validJson = "{\"enable\": true, \"config\": {\"key1\": \"value1\", \"key2\": true}}";
+ assertThat(fromString(validJson).getConfig().get("key1")).isEqualTo("value1");
+ }
+
+ @Test
+ void testIsValidEmptyString() {
+ assertThatThrownBy(() -> fromString(""))
+ .as("Validating empty string should throw InvalidPolicyException")
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Policy is empty");
+ }
+
+ @Test
+ void testIsValidEmptyJson() {
+ assertThatThrownBy(() -> fromString("{}"))
+ .as("Validating empty JSON '{}' should throw InvalidPolicyException")
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Invalid policy");
+ }
+
+ @Test
+ void testIsValidInvalidVersionFormat() {
+ String invalidPolicy1 = "{\"enable\": true, \"version\": \"fdafds\"}";
+ assertThatThrownBy(() -> fromString(invalidPolicy1))
+ .as("Validating policy with invalid version format should throw InvalidPolicyException")
+ .isInstanceOf(InvalidPolicyException.class);
+ }
+
+ @Test
+ void testIsValidInvalidKeyInPolicy() {
+ String invalidPolicy2 =
+ "{\"version\":\"2025-02-03\", \"enable\": true, \"invalid_key\": 12342}";
+ assertThatThrownBy(() -> fromString(invalidPolicy2))
+ .as("Validating policy with an unknown key should throw InvalidPolicyException")
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Invalid policy");
+ }
+
+ @Test
+ void testIsValidUnrecognizedToken() {
+ var invalidPolicy = "{\"enable\": invalidToken}";
+ assertThatThrownBy(() -> fromString(invalidPolicy))
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Invalid policy");
+ }
+
+ @Test
+ void testIsValidNullValue() {
+ var invalidPolicy = "{\"enable\": null}";
+ assertThatThrownBy(() -> fromString(invalidPolicy))
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Invalid policy");
+ }
+
+ @Test
+ void testIsValidWrongString() {
+ var invalidPolicy = "{\"enable\": \"invalid\"}";
+ assertThatThrownBy(() -> fromString(invalidPolicy))
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Invalid policy");
+ }
+}
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java
new file mode 100644
index 000000000..e833015d6
--- /dev/null
+++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import static org.apache.polaris.core.entity.PolarisEntitySubType.ANY_SUBTYPE;
+import static org.apache.polaris.core.entity.PolarisEntitySubType.TABLE;
+import static org.apache.polaris.core.entity.PolarisEntitySubType.VIEW;
+import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG;
+import static org.apache.polaris.core.entity.PolarisEntityType.ICEBERG_TABLE_LIKE;
+import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE;
+import static org.apache.polaris.core.entity.PolarisEntityType.PRINCIPAL;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyValidator;
+import org.junit.jupiter.api.Test;
+
+public class DataCompactionPolicyValidatorTest {
+ private final DataCompactionPolicyValidator validator = new DataCompactionPolicyValidator();
+
+ @Test
+ public void testValidPolicies() {
+ var validJson = "{\"version\":\"2025-02-03\", \"enable\": true}";
+ validator.validate(validJson);
+
+ assertThatThrownBy(() -> validator.validate(""))
+ .as("Validating empty string should throw InvalidPolicyException")
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Policy is empty");
+ }
+
+ @Test
+ public void testCanAttachReturnsTrueForCatalogType() {
+ var result = validator.canAttach(CATALOG, ANY_SUBTYPE); // using any valid subtype
+ assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type");
+ }
+
+ @Test
+ public void testCanAttachReturnsTrueForNamespaceType() {
+ var result = validator.canAttach(NAMESPACE, ANY_SUBTYPE); // using any valid subtype
+ assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type");
+ }
+
+ @Test
+ public void testCanAttachReturnsTrueForIcebergTableLikeWithTableSubtype() {
+ var result = validator.canAttach(ICEBERG_TABLE_LIKE, TABLE);
+ assertThat(result)
+ .isTrue()
+ .as("Expected canAttach() to return true for ICEBERG_TABLE_LIKE with TABLE subtype");
+ }
+
+ @Test
+ public void testCanAttachReturnsFalseForIcebergTableLikeWithNonTableSubtype() {
+ // For ICEBERG_TABLE_LIKE, any subtype other than TABLE should return false.
+ boolean result = validator.canAttach(ICEBERG_TABLE_LIKE, VIEW);
+ assertThat(result)
+ .isFalse()
+ .as("Expected canAttach() to return false for ICEBERG_TABLE_LIKE with non-TABLE subtype");
+ }
+
+ @Test
+ public void testCanAttachReturnsFalseForNull() {
+ var result = validator.canAttach(null, null); // using any valid subtype
+ assertThat(result).isFalse().as("Expected canAttach() to return false for null");
+ }
+
+ @Test
+ public void testCanAttachReturnsFalseForUnattachableType() {
+ var result = validator.canAttach(PRINCIPAL, null); // using any valid subtype
+ assertThat(result).isFalse().as("Expected canAttach() to return false for null");
+ }
+}
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java
new file mode 100644
index 000000000..24d122a11
--- /dev/null
+++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.polaris.core.policy.validator;
+
+import static org.apache.polaris.core.policy.PredefinedPolicyTypes.DATA_COMPACTION;
+import static org.apache.polaris.core.policy.PredefinedPolicyTypes.METADATA_COMPACTION;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.polaris.core.policy.PolicyEntity;
+import org.junit.jupiter.api.Test;
+
+public class PolicyValidatorsTest {
+ @Test
+ public void testInvalidPolicy() {
+ var policyEntity =
+ new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", DATA_COMPACTION)
+ .setContent("InvalidContent")
+ .setPolicyVersion(0)
+ .build();
+ assertThatThrownBy(() -> PolicyValidators.validate(policyEntity))
+ .as("Validating empty JSON '{}' should throw InvalidPolicyException")
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Invalid policy");
+ }
+
+ @Test
+ public void testUnsupportedPolicyType() {
+ var policyEntity =
+ new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", METADATA_COMPACTION)
+ .setContent("InvalidContent")
+ .setPolicyVersion(0)
+ .build();
+
+ assertThatThrownBy(() -> PolicyValidators.validate(policyEntity))
+ .isInstanceOf(InvalidPolicyException.class)
+ .hasMessageContaining("Unsupported policy type");
+ }
+
+ @Test
+ public void testValidPolicy() {
+ var policyEntity =
+ new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", DATA_COMPACTION)
+ .setContent("{\"enable\": false}")
+ .setPolicyVersion(0)
+ .build();
+ PolicyValidators.validate(policyEntity);
+ }
+}
diff --git a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
index 7686f9f75..0636f5b97 100644
--- a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
+++ b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
@@ -25,6 +25,7 @@
import org.apache.iceberg.rest.responses.ErrorResponse;
import org.apache.polaris.core.exceptions.AlreadyExistsException;
import org.apache.polaris.core.exceptions.PolarisException;
+import org.apache.polaris.core.policy.validator.InvalidPolicyException;
import org.apache.polaris.service.context.UnresolvableRealmContextException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,6 +45,8 @@ private Response.Status getStatus(PolarisException exception) {
return Response.Status.CONFLICT;
} else if (exception instanceof UnresolvableRealmContextException) {
return Response.Status.NOT_FOUND;
+ } else if (exception instanceof InvalidPolicyException) {
+ return Response.Status.BAD_REQUEST;
} else {
return Response.Status.INTERNAL_SERVER_ERROR;
}