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

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


The following commit(s) were added to refs/heads/main by this push:
     new f3384494f3 [#12379] feat(core): Support assignment values in common 
and core (#12380)
f3384494f3 is described below

commit f3384494f3d537aa16e800f1c58a44e26bb7cd16
Author: roryqi <[email protected]>
AuthorDate: Thu Aug 6 14:11:15 2026 +0800

    [#12379] feat(core): Support assignment values in common and core (#12380)
    
    ### What changes were proposed in this pull request?
    
    Support tag assignment values in the common and core layers:
    
    - Add request and response DTO support for tag value constraints and
    assignment values.
    - Implement valued tag association and exact-value lookup in the tag
    manager and dispatcher.
    - Preserve assignment values through hooks and listener events.
    - Preserve direct assignments when inherited assignments use different
    values.
    - Validate and deduplicate valued assignments.
    
    The REST exposure and version-specific request handling will follow in a
    separate PR.
    
    ### Why are the changes needed?
    
    The tag assignment value API and relational storage support have been
    merged. The common and core layers need to connect those APIs before the
    server REST layer can expose them.
    
    Related to #12379
    
    ### Does this PR introduce _any_ user-facing change?
    
    No direct REST behavior change is exposed by this PR. It provides the
    common DTO and core behavior required by the follow-up server PR.
    
    ### How was this patch tested?
    
    - `./gradlew :common:test --tests
    org.apache.gravitino.dto.requests.TestTagCreateRequest --tests
    org.apache.gravitino.dto.requests.TestTagsAssociateRequest --tests
    org.apache.gravitino.dto.requests.TestTagValuesAssociateRequest --tests
    org.apache.gravitino.dto.tag.TestTagDTO :core:test --tests
    org.apache.gravitino.tag.TestTagManager --tests
    org.apache.gravitino.hook.TestTagHookDispatcher --tests
    org.apache.gravitino.listener.api.event.TestTagEvent -PskipITs
    -PskipDockerTests=false`
---
 .../gravitino/dto/requests/TagCreateRequest.java   |  49 +++++-
 .../dto/requests/TagValuesAssociateRequest.java    | 195 +++++++++++++++++++++
 .../java/org/apache/gravitino/dto/tag/TagDTO.java  |  58 +++++-
 .../apache/gravitino/dto/util/DTOConverters.java   |  18 ++
 .../dto/requests/TestTagCreateRequest.java         |  39 +++++
 .../requests/TestTagValuesAssociateRequest.java    |  75 ++++++++
 .../dto/requests/TestTagsAssociateRequest.java     |  54 ++++++
 .../org/apache/gravitino/dto/tag/TestTagDTO.java   |  44 +++++
 .../apache/gravitino/hook/TagHookDispatcher.java   |  29 ++-
 .../gravitino/listener/TagEventDispatcher.java     | 117 +++++++++++--
 .../event/AssociateTagsForMetadataObjectEvent.java |  80 ++++++++-
 ...AssociateTagsForMetadataObjectFailureEvent.java |  73 +++++++-
 .../AssociateTagsForMetadataObjectPreEvent.java    |  70 +++++++-
 .../gravitino/listener/api/info/TagInfo.java       |  58 ++++++
 .../org/apache/gravitino/tag/TagDispatcher.java    |  60 +++++++
 .../java/org/apache/gravitino/tag/TagManager.java  | 144 ++++++++++++---
 .../gravitino/hook/TestTagHookDispatcher.java      |   4 +-
 .../gravitino/listener/api/event/TestTagEvent.java |  64 ++++++-
 .../org/apache/gravitino/tag/TestTagManager.java   | 190 ++++++++++++++++++--
 19 files changed, 1345 insertions(+), 76 deletions(-)

diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java
index 5b786f47c8..09577575d2 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java
@@ -27,6 +27,7 @@ import lombok.Getter;
 import lombok.ToString;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.tag.TagValueConstraint;
 
 /** Represents a request to create a tag. */
 @Getter
@@ -34,6 +35,8 @@ import org.apache.gravitino.rest.RESTRequest;
 @ToString
 public class TagCreateRequest implements RESTRequest {
 
+  private static final int MAX_ALLOWED_VALUE_LENGTH = 256;
+
   @JsonProperty("name")
   private final String name;
 
@@ -45,6 +48,10 @@ public class TagCreateRequest implements RESTRequest {
   @Nullable
   private Map<String, String> properties;
 
+  @JsonProperty("allowedValues")
+  @Nullable
+  private final String[] allowedValues;
+
   /**
    * Creates a new TagCreateRequest.
    *
@@ -53,14 +60,43 @@ public class TagCreateRequest implements RESTRequest {
    * @param properties The properties of the tag.
    */
   public TagCreateRequest(String name, String comment, Map<String, String> 
properties) {
+    this(name, comment, properties, null);
+  }
+
+  /**
+   * Creates a new TagCreateRequest.
+   *
+   * @param name The name of the tag.
+   * @param comment The comment of the tag.
+   * @param properties The properties of the tag.
+   * @param allowedValues The allowed assignment values of the tag.
+   */
+  public TagCreateRequest(
+      String name, String comment, Map<String, String> properties, String[] 
allowedValues) {
     this.name = name;
     this.comment = comment;
     this.properties = properties;
+    this.allowedValues = allowedValues;
   }
 
   /** This is the constructor that is used by Jackson deserializer */
   public TagCreateRequest() {
-    this(null, null, null);
+    this(null, null, null, null);
+  }
+
+  /**
+   * Returns the tag value constraint represented by this request.
+   *
+   * @return The tag value constraint.
+   */
+  public TagValueConstraint valueConstraint() {
+    if (allowedValues == null) {
+      return TagValueConstraint.anyValue();
+    }
+    if (allowedValues.length == 0) {
+      return TagValueConstraint.noValue();
+    }
+    return TagValueConstraint.ofAllowedValues(allowedValues);
   }
 
   /**
@@ -72,5 +108,16 @@ public class TagCreateRequest implements RESTRequest {
   public void validate() throws IllegalArgumentException {
     Preconditions.checkArgument(
         StringUtils.isNotBlank(name), "\"name\" is required and cannot be 
empty");
+
+    if (allowedValues != null) {
+      for (String value : allowedValues) {
+        Preconditions.checkArgument(
+            StringUtils.isNotBlank(value), "allowedValues cannot contain null 
or empty values");
+        Preconditions.checkArgument(
+            value.length() <= MAX_ALLOWED_VALUE_LENGTH,
+            "Each allowed value must not exceed %s characters",
+            MAX_ALLOWED_VALUE_LENGTH);
+      }
+    }
   }
 }
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/TagValuesAssociateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagValuesAssociateRequest.java
new file mode 100644
index 0000000000..8eaf2720bc
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagValuesAssociateRequest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.tag.TagValue;
+
+/** Represents a request to associate tags with optional values. */
+public class TagValuesAssociateRequest implements RESTRequest {
+
+  private static final int MAX_TAG_VALUE_LENGTH = 256;
+
+  @JsonProperty("tagsToAdd")
+  private final RequestTagValue[] tagsToAdd;
+
+  @JsonProperty("tagsToRemove")
+  private final RequestTagValue[] tagsToRemove;
+
+  /**
+   * Creates a new TagValuesAssociateRequest.
+   *
+   * @param tagsToAdd The tag values to add.
+   * @param tagsToRemove The tag values to remove.
+   */
+  public TagValuesAssociateRequest(TagValue[] tagsToAdd, TagValue[] 
tagsToRemove) {
+    this.tagsToAdd = toRequestTagValues(tagsToAdd);
+    this.tagsToRemove = toRequestTagValues(tagsToRemove);
+  }
+
+  /** This is the constructor that is used by Jackson deserializer */
+  public TagValuesAssociateRequest() {
+    this.tagsToAdd = null;
+    this.tagsToRemove = null;
+  }
+
+  /**
+   * Returns the tag values to add.
+   *
+   * @return The tag values to add.
+   */
+  public TagValue[] tagValuesToAdd() {
+    return toTagValues(tagsToAdd);
+  }
+
+  /**
+   * Returns the tag values to remove.
+   *
+   * @return The tag values to remove.
+   */
+  public TagValue[] tagValuesToRemove() {
+    return toTagValues(tagsToRemove);
+  }
+
+  /**
+   * Validates the request.
+   *
+   * @throws IllegalArgumentException If the request is invalid, this 
exception is thrown.
+   */
+  @Override
+  public void validate() throws IllegalArgumentException {
+    Preconditions.checkArgument(
+        tagsToAdd != null || tagsToRemove != null,
+        "tagsToAdd and tagsToRemove cannot both be null");
+
+    validateTagValues(tagsToAdd, "tagsToAdd");
+    validateTagValues(tagsToRemove, "tagsToRemove");
+  }
+
+  private static RequestTagValue[] toRequestTagValues(TagValue[] tagValues) {
+    if (tagValues == null) {
+      return null;
+    }
+    return 
Arrays.stream(tagValues).map(RequestTagValue::new).toArray(RequestTagValue[]::new);
+  }
+
+  private static TagValue[] toTagValues(RequestTagValue[] tagValues) {
+    if (tagValues == null) {
+      return null;
+    }
+    return 
Arrays.stream(tagValues).map(RequestTagValue::toTagValue).toArray(TagValue[]::new);
+  }
+
+  private static void validateTagValues(RequestTagValue[] tagValues, String 
fieldName) {
+    if (tagValues == null) {
+      return;
+    }
+
+    for (RequestTagValue tagValue : tagValues) {
+      Preconditions.checkArgument(
+          tagValue != null, "%s must not contain null tag values", fieldName);
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(tagValue.name),
+          "%s must not contain null or empty tag names",
+          fieldName);
+      if (tagValue.value != null) {
+        Preconditions.checkArgument(
+            StringUtils.isNotBlank(tagValue.value),
+            "%s must not contain null or empty tag values",
+            fieldName);
+        Preconditions.checkArgument(
+            tagValue.value.length() <= MAX_TAG_VALUE_LENGTH,
+            "%s tag values must not exceed %s characters",
+            fieldName,
+            MAX_TAG_VALUE_LENGTH);
+      }
+    }
+  }
+  /**
+   * Compares this request with another object.
+   *
+   * @param o The object to compare.
+   * @return True if the object is equal to this request.
+   */
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TagValuesAssociateRequest)) {
+      return false;
+    }
+
+    TagValuesAssociateRequest that = (TagValuesAssociateRequest) o;
+    return Arrays.equals(tagsToAdd, that.tagsToAdd)
+        && Arrays.equals(tagsToRemove, that.tagsToRemove);
+  }
+
+  /**
+   * @return The hash code of this request.
+   */
+  @Override
+  public int hashCode() {
+    int result = Arrays.hashCode(tagsToAdd);
+    result = 31 * result + Arrays.hashCode(tagsToRemove);
+    return result;
+  }
+
+  /**
+   * @return The string representation of this request.
+   */
+  @Override
+  public String toString() {
+    return "TagValuesAssociateRequest{"
+        + "tagsToAdd="
+        + Arrays.toString(tagsToAdd)
+        + ", tagsToRemove="
+        + Arrays.toString(tagsToRemove)
+        + "}";
+  }
+
+  @EqualsAndHashCode
+  @ToString
+  static class RequestTagValue {
+    @JsonProperty("name")
+    private String name;
+
+    @JsonProperty("value")
+    @Nullable
+    private String value;
+
+    private RequestTagValue() {}
+
+    private RequestTagValue(TagValue tagValue) {
+      this.name = tagValue.name();
+      this.value = tagValue.value().orElse(null);
+    }
+
+    private TagValue toTagValue() {
+      return value == null ? TagValue.noValue(name) : TagValue.of(name, value);
+    }
+  }
+}
diff --git a/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
index d0e8fb6eee..31c608b896 100644
--- a/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
@@ -20,10 +20,13 @@ package org.apache.gravitino.dto.tag;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Objects;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.Optional;
 import org.apache.gravitino.dto.AuditDTO;
 import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagAssignment;
+import org.apache.gravitino.tag.TagValueConstraint;
 
 /** Represents a Tag Data Transfer Object (DTO). */
 public class TagDTO implements Tag {
@@ -37,6 +40,12 @@ public class TagDTO implements Tag {
   @JsonProperty("properties")
   private Map<String, String> properties;
 
+  @JsonProperty("allowedValues")
+  private String[] allowedValues;
+
+  @JsonProperty("assignmentValues")
+  private String[] assignmentValues;
+
   @JsonProperty("audit")
   private AuditDTO audit;
 
@@ -60,6 +69,28 @@ public class TagDTO implements Tag {
     return properties;
   }
 
+  @Override
+  public TagValueConstraint valueConstraint() {
+    if (allowedValues == null) {
+      return TagValueConstraint.anyValue();
+    }
+    if (allowedValues.length == 0) {
+      return TagValueConstraint.noValue();
+    }
+    return TagValueConstraint.ofAllowedValues(allowedValues);
+  }
+
+  @Override
+  public Optional<TagAssignment> assignment() {
+    if (assignmentValues == null) {
+      return Optional.empty();
+    }
+    if (assignmentValues.length == 0) {
+      return Optional.of(TagAssignment.noValue());
+    }
+    return Optional.of(TagAssignment.ofValues(assignmentValues));
+  }
+
   @Override
   public AuditDTO auditInfo() {
     return audit;
@@ -83,12 +114,15 @@ public class TagDTO implements Tag {
     return Objects.equal(name, tagDTO.name)
         && Objects.equal(comment, tagDTO.comment)
         && Objects.equal(properties, tagDTO.properties)
+        && Arrays.equals(allowedValues, tagDTO.allowedValues)
         && Objects.equal(audit, tagDTO.audit);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(name, comment, properties, audit);
+    int result = Objects.hashCode(name, comment, properties, audit);
+    result = 31 * result + Arrays.hashCode(allowedValues);
+    return result;
   }
 
   /**
@@ -139,6 +173,28 @@ public class TagDTO implements Tag {
       return this;
     }
 
+    /**
+     * Sets the allowed assignment values for the tag.
+     *
+     * @param allowedValues The allowed assignment values for the tag.
+     * @return The builder instance.
+     */
+    public Builder withAllowedValues(String[] allowedValues) {
+      tagDTO.allowedValues = allowedValues == null ? null : 
allowedValues.clone();
+      return this;
+    }
+
+    /**
+     * Sets the assignment values for the tag.
+     *
+     * @param assignmentValues The assignment values for the tag on a metadata 
object.
+     * @return The builder instance.
+     */
+    public Builder withAssignmentValues(String[] assignmentValues) {
+      tagDTO.assignmentValues = assignmentValues == null ? null : 
assignmentValues.clone();
+      return this;
+    }
+
     /**
      * Sets the audit information for the tag.
      *
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java 
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index 864823a77b..b30ba533af 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -130,6 +130,7 @@ import org.apache.gravitino.rel.partitions.RangePartition;
 import org.apache.gravitino.rel.types.Types;
 import org.apache.gravitino.stats.Statistic;
 import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagValueConstraint;
 
 /** Utility class for converting between DTOs and domain objects. */
 public class DTOConverters {
@@ -609,12 +610,29 @@ public class DTOConverters {
             .withName(tag.name())
             .withComment(tag.comment())
             .withProperties(tag.properties())
+            .withAllowedValues(allowedValuesForDTO(tag.valueConstraint()))
+            .withAssignmentValues(
+                tag.assignment().map(assignment -> 
assignment.values()).orElse(null))
             .withAudit(toDTO(tag.auditInfo()))
             .withInherited(inherited);
 
     return builder.build();
   }
 
+  private static String[] allowedValuesForDTO(TagValueConstraint 
valueConstraint) {
+    TagValueConstraint normalizedConstraint =
+        valueConstraint == null ? TagValueConstraint.anyValue() : 
valueConstraint;
+    switch (normalizedConstraint.type()) {
+      case ANY_VALUE:
+        return null;
+      case NO_VALUE:
+      case ALLOWED_VALUES:
+        return normalizedConstraint.allowedValues();
+      default:
+        throw new IllegalArgumentException("Unknown tag value constraint: " + 
normalizedConstraint);
+    }
+  }
+
   /**
    * Converts a PolicyContent to a PolicyContentDTO.
    *
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
index 5d90639ca5..3ce544e127 100644
--- 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.dto.requests;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
 import java.util.Map;
 import org.apache.gravitino.json.JsonUtils;
 import org.junit.jupiter.api.Assertions;
@@ -46,4 +47,42 @@ public class TestTagCreateRequest {
     Assertions.assertEquals(request1, deserRequest1);
     Assertions.assertEquals(properties, deserRequest1.getProperties());
   }
+
+  @Test
+  public void testTagCreateRequestSerDeWithAllowedValues() throws 
JsonProcessingException {
+    String[] allowedValues = new String[] {"finance", "risk"};
+    TagCreateRequest request = new TagCreateRequest("tag_test", "tag comment", 
null, allowedValues);
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+    TagCreateRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, TagCreateRequest.class);
+
+    Assertions.assertEquals(request, deserRequest);
+    Assertions.assertArrayEquals(allowedValues, 
deserRequest.getAllowedValues());
+  }
+
+  @Test
+  public void testTagCreateRequestValidateAllowedValues() {
+    new TagCreateRequest("tag_test", "tag comment", null, new String[] 
{"finance"}).validate();
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> new TagCreateRequest("tag_test", null, null, new String[] 
{""}).validate());
+    char[] maxLengthValueChars = new char[256];
+    Arrays.fill(maxLengthValueChars, 'a');
+    Assertions.assertDoesNotThrow(
+        () ->
+            new TagCreateRequest(
+                    "tag_test", null, null, new String[] {new 
String(maxLengthValueChars)})
+                .validate());
+
+    char[] tooLongValueChars = new char[257];
+    Arrays.fill(tooLongValueChars, 'a');
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            new TagCreateRequest(
+                    "tag_test", null, null, new String[] {new 
String(tooLongValueChars)})
+                .validate());
+  }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagValuesAssociateRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagValuesAssociateRequest.java
new file mode 100644
index 0000000000..970ca6ca2a
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagValuesAssociateRequest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.core.JsonProcessingException;
+import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.tag.TagValue;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagValuesAssociateRequest {
+
+  @Test
+  public void testTagValuesAssociateRequestSerDe() throws 
JsonProcessingException {
+    TagValuesAssociateRequest request =
+        new TagValuesAssociateRequest(
+            new TagValue[] {TagValue.noValue("pii"), 
TagValue.of("data_domain", "finance")},
+            new TagValue[] {TagValue.of("data_domain", "old")});
+
+    String json = JsonUtils.objectMapper().writeValueAsString(request);
+    TagValuesAssociateRequest deserialized =
+        JsonUtils.objectMapper().readValue(json, 
TagValuesAssociateRequest.class);
+
+    Assertions.assertEquals(request, deserialized);
+    Assertions.assertArrayEquals(request.tagValuesToAdd(), 
deserialized.tagValuesToAdd());
+    Assertions.assertArrayEquals(request.tagValuesToRemove(), 
deserialized.tagValuesToRemove());
+  }
+
+  @Test
+  public void testTagValuesAssociateRequestValidateValues() throws 
JsonProcessingException {
+    TagValuesAssociateRequest validRequest =
+        new TagValuesAssociateRequest(new TagValue[] 
{TagValue.of("data_domain", "finance")}, null);
+    Assertions.assertDoesNotThrow(validRequest::validate);
+
+    TagValuesAssociateRequest blankNameRequest =
+        JsonUtils.objectMapper()
+            .readValue(
+                "{\"tagsToAdd\":[{\"name\":\" \",\"value\":\"finance\"}]}",
+                TagValuesAssociateRequest.class);
+    Assertions.assertThrows(IllegalArgumentException.class, 
blankNameRequest::validate);
+
+    TagValuesAssociateRequest blankValueRequest =
+        JsonUtils.objectMapper()
+            .readValue(
+                "{\"tagsToAdd\":[{\"name\":\"data_domain\",\"value\":\" \"}]}",
+                TagValuesAssociateRequest.class);
+    Assertions.assertThrows(IllegalArgumentException.class, 
blankValueRequest::validate);
+  }
+
+  @Test
+  public void testTagValuesAssociateRequestRejectsV1Shape() {
+    Assertions.assertThrows(
+        JsonProcessingException.class,
+        () ->
+            JsonUtils.objectMapper()
+                .readValue("{\"tagsToAdd\":[\"data_domain\"]}", 
TagValuesAssociateRequest.class));
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagsAssociateRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagsAssociateRequest.java
new file mode 100644
index 0000000000..47635bf33d
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagsAssociateRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.core.JsonProcessingException;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagsAssociateRequest {
+
+  @Test
+  public void testTagsAssociateRequestSerDe() throws JsonProcessingException {
+    String[] tagsToAdd = {"pii", "data_domain"};
+    String[] tagsToRemove = {"deprecated"};
+    TagsAssociateRequest request = new TagsAssociateRequest(tagsToAdd, 
tagsToRemove);
+
+    String json = JsonUtils.objectMapper().writeValueAsString(request);
+    TagsAssociateRequest deserialized =
+        JsonUtils.objectMapper().readValue(json, TagsAssociateRequest.class);
+
+    Assertions.assertEquals(request, deserialized);
+    Assertions.assertArrayEquals(tagsToAdd, deserialized.getTagsToAdd());
+    Assertions.assertArrayEquals(tagsToRemove, deserialized.getTagsToRemove());
+  }
+
+  @Test
+  public void testTagsAssociateRequestRejectsV2Shape() {
+    Assertions.assertThrows(
+        JsonProcessingException.class,
+        () ->
+            JsonUtils.objectMapper()
+                .readValue(
+                    
"{\"tagsToAdd\":[{\"name\":\"data_domain\",\"value\":\"finance\"}]}",
+                    TagsAssociateRequest.class));
+  }
+}
diff --git a/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java 
b/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java
index 89acaa887d..8d39b2996f 100644
--- a/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java
+++ b/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java
@@ -62,6 +62,24 @@ public class TestTagDTO {
 
     Assertions.assertEquals(properties, deserTagDTO1.properties());
 
+    // Test tag with allowed values and assignment values
+    TagDTO tagWithValues =
+        TagDTO.builder()
+            .withName("tag_test")
+            .withComment("tag comment")
+            .withAudit(audit)
+            .withAllowedValues(new String[] {"finance", "risk"})
+            .withAssignmentValues(new String[] {"finance"})
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(tagWithValues);
+    TagDTO deserTagWithValues = JsonUtils.objectMapper().readValue(serJson, 
TagDTO.class);
+    Assertions.assertEquals(tagWithValues, deserTagWithValues);
+    Assertions.assertArrayEquals(
+        new String[] {"finance", "risk"}, 
deserTagWithValues.valueConstraint().allowedValues());
+    Assertions.assertArrayEquals(
+        new String[] {"finance"}, 
deserTagWithValues.assignment().get().values());
+
     // Test tag with inherited
     TagDTO tagDTO2 =
         TagDTO.builder()
@@ -100,4 +118,30 @@ public class TestTagDTO {
     TagDTO deserTagDTO4 = JsonUtils.objectMapper().readValue(serJson, 
TagDTO.class);
     Assertions.assertEquals(Optional.of(true), deserTagDTO4.inherited());
   }
+
+  @Test
+  public void testAssignmentValuesDoNotAffectEquality() {
+    AuditDTO audit = 
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+    TagDTO globalTag =
+        TagDTO.builder().withName("tag_test").withComment("tag 
comment").withAudit(audit).build();
+    TagDTO assignedTag =
+        TagDTO.builder()
+            .withName("tag_test")
+            .withComment("tag comment")
+            .withAudit(audit)
+            .withAssignmentValues(new String[0])
+            .build();
+    TagDTO valuedAssignedTag =
+        TagDTO.builder()
+            .withName("tag_test")
+            .withComment("tag comment")
+            .withAudit(audit)
+            .withAssignmentValues(new String[] {"finance"})
+            .build();
+
+    Assertions.assertEquals(globalTag, assignedTag);
+    Assertions.assertEquals(globalTag, valuedAssignedTag);
+    Assertions.assertEquals(globalTag.hashCode(), assignedTag.hashCode());
+    Assertions.assertEquals(globalTag.hashCode(), 
valuedAssignedTag.hashCode());
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java 
b/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
index 75bfbd1fb3..fd71f4a961 100644
--- a/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
@@ -28,6 +28,8 @@ import 
org.apache.gravitino.exceptions.TagAlreadyExistsException;
 import org.apache.gravitino.tag.Tag;
 import org.apache.gravitino.tag.TagChange;
 import org.apache.gravitino.tag.TagDispatcher;
+import org.apache.gravitino.tag.TagValue;
+import org.apache.gravitino.tag.TagValueConstraint;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.apache.gravitino.utils.PrincipalUtils;
 
@@ -57,7 +59,17 @@ public class TagHookDispatcher implements TagDispatcher {
   @Override
   public Tag createTag(
       String metalake, String name, String comment, Map<String, String> 
properties) {
-    Tag tag = dispatcher.createTag(metalake, name, comment, properties);
+    return createTag(metalake, name, comment, properties, 
TagValueConstraint.anyValue());
+  }
+
+  @Override
+  public Tag createTag(
+      String metalake,
+      String name,
+      String comment,
+      Map<String, String> properties,
+      TagValueConstraint valueConstraint) {
+    Tag tag = dispatcher.createTag(metalake, name, comment, properties, 
valueConstraint);
 
     // Set the creator as the owner of the tag.
     OwnerDispatcher ownerDispatcher = 
GravitinoEnv.getInstance().ownerDispatcher();
@@ -89,6 +101,11 @@ public class TagHookDispatcher implements TagDispatcher {
     return dispatcher.listMetadataObjectsForTag(metalake, name);
   }
 
+  @Override
+  public MetadataObject[] listMetadataObjectsForTag(String metalake, String 
name, String value) {
+    return dispatcher.listMetadataObjectsForTag(metalake, name, value);
+  }
+
   @Override
   public String[] listTagsForMetadataObject(String metalake, MetadataObject 
metadataObject) {
     return dispatcher.listTagsForMetadataObject(metalake, metadataObject);
@@ -106,6 +123,16 @@ public class TagHookDispatcher implements TagDispatcher {
         metalake, metadataObject, tagsToAdd, tagsToRemove);
   }
 
+  @Override
+  public String[] associateTagValuesForMetadataObject(
+      String metalake,
+      MetadataObject metadataObject,
+      TagValue[] tagsToAdd,
+      TagValue[] tagsToRemove) {
+    return dispatcher.associateTagValuesForMetadataObject(
+        metalake, metadataObject, tagsToAdd, tagsToRemove);
+  }
+
   @Override
   public Tag getTagForMetadataObject(String metalake, MetadataObject 
metadataObject, String name) {
     return dispatcher.getTagForMetadataObject(metalake, metadataObject, name);
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java 
b/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java
index 866af597a2..2df08a2d38 100644
--- a/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java
@@ -59,6 +59,8 @@ import org.apache.gravitino.listener.api.info.TagInfo;
 import org.apache.gravitino.tag.Tag;
 import org.apache.gravitino.tag.TagChange;
 import org.apache.gravitino.tag.TagDispatcher;
+import org.apache.gravitino.tag.TagValue;
+import org.apache.gravitino.tag.TagValueConstraint;
 import org.apache.gravitino.utils.PrincipalUtils;
 
 /**
@@ -115,7 +117,7 @@ public class TagEventDispatcher implements TagDispatcher {
     eventBus.dispatchEvent(new 
GetTagPreEvent(PrincipalUtils.getCurrentUserName(), metalake, name));
     try {
       Tag tag = dispatcher.getTag(metalake, name);
-      TagInfo tagInfo = new TagInfo(tag.name(), tag.comment(), 
tag.properties());
+      TagInfo tagInfo = tagInfo(tag);
       eventBus.dispatchEvent(
           new GetTagEvent(PrincipalUtils.getCurrentUserName(), metalake, name, 
tagInfo));
       return tag;
@@ -129,16 +131,24 @@ public class TagEventDispatcher implements TagDispatcher {
   @Override
   public Tag createTag(
       String metalake, String name, String comment, Map<String, String> 
properties) {
-    TagInfo tagInfo = new TagInfo(name, comment, properties);
+    return createTag(metalake, name, comment, properties, 
TagValueConstraint.anyValue());
+  }
+
+  @Override
+  public Tag createTag(
+      String metalake,
+      String name,
+      String comment,
+      Map<String, String> properties,
+      TagValueConstraint valueConstraint) {
+    TagInfo tagInfo =
+        new TagInfo(name, comment, properties, 
allowedValuesForInfo(valueConstraint), null);
     eventBus.dispatchEvent(
         new CreateTagPreEvent(PrincipalUtils.getCurrentUserName(), metalake, 
tagInfo));
     try {
-      Tag tag = dispatcher.createTag(metalake, name, comment, properties);
+      Tag tag = dispatcher.createTag(metalake, name, comment, properties, 
valueConstraint);
       eventBus.dispatchEvent(
-          new CreateTagEvent(
-              PrincipalUtils.getCurrentUserName(),
-              metalake,
-              new TagInfo(tag.name(), tag.comment(), tag.properties())));
+          new CreateTagEvent(PrincipalUtils.getCurrentUserName(), metalake, 
tagInfo(tag)));
       return tag;
     } catch (Exception e) {
       eventBus.dispatchEvent(
@@ -157,11 +167,7 @@ public class TagEventDispatcher implements TagDispatcher {
     try {
       Tag tag = dispatcher.alterTag(metalake, name, changes);
       eventBus.dispatchEvent(
-          new AlterTagEvent(
-              PrincipalUtils.getCurrentUserName(),
-              metalake,
-              changes,
-              new TagInfo(tag.name(), tag.comment(), tag.properties())));
+          new AlterTagEvent(PrincipalUtils.getCurrentUserName(), metalake, 
changes, tagInfo(tag)));
       return tag;
     } catch (Exception e) {
       eventBus.dispatchEvent(
@@ -210,6 +216,28 @@ public class TagEventDispatcher implements TagDispatcher {
     }
   }
 
+  @Override
+  public MetadataObject[] listMetadataObjectsForTag(String metalake, String 
name, String value) {
+    eventBus.dispatchEvent(
+        new 
ListMetadataObjectsForTagPreEvent(PrincipalUtils.getCurrentUserName(), 
metalake, name));
+    try {
+      MetadataObject[] metadataObjects =
+          dispatcher.listMetadataObjectsForTag(metalake, name, value);
+      eventBus.dispatchEvent(
+          new ListMetadataObjectsForTagEvent(
+              PrincipalUtils.getCurrentUserName(),
+              metalake,
+              name,
+              metadataObjects != null ? metadataObjects.length : -1));
+      return metadataObjects;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(
+          new ListMetadataObjectsForTagFailureEvent(
+              PrincipalUtils.getCurrentUserName(), metalake, name, e));
+      throw e;
+    }
+  }
+
   @Override
   public String[] listTagsForMetadataObject(String metalake, MetadataObject 
metadataObject) {
     eventBus.dispatchEvent(
@@ -292,6 +320,46 @@ public class TagEventDispatcher implements TagDispatcher {
     }
   }
 
+  @Override
+  public String[] associateTagValuesForMetadataObject(
+      String metalake,
+      MetadataObject metadataObject,
+      TagValue[] tagsToAdd,
+      TagValue[] tagsToRemove) {
+    eventBus.dispatchEvent(
+        new AssociateTagsForMetadataObjectPreEvent(
+            PrincipalUtils.getCurrentUserName(),
+            metalake,
+            metadataObject,
+            tagsToAdd,
+            tagsToRemove));
+
+    try {
+      String[] associatedTags =
+          dispatcher.associateTagValuesForMetadataObject(
+              metalake, metadataObject, tagsToAdd, tagsToRemove);
+      eventBus.dispatchEvent(
+          new AssociateTagsForMetadataObjectEvent(
+              PrincipalUtils.getCurrentUserName(),
+              metalake,
+              metadataObject,
+              tagsToAdd,
+              tagsToRemove,
+              associatedTags));
+      return associatedTags;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(
+          new AssociateTagsForMetadataObjectFailureEvent(
+              PrincipalUtils.getCurrentUserName(),
+              metalake,
+              metadataObject,
+              tagsToAdd,
+              tagsToRemove,
+              e));
+      throw e;
+    }
+  }
+
   @Override
   public Tag getTagForMetadataObject(String metalake, MetadataObject 
metadataObject, String name) {
     eventBus.dispatchEvent(
@@ -299,7 +367,7 @@ public class TagEventDispatcher implements TagDispatcher {
             PrincipalUtils.getCurrentUserName(), metalake, metadataObject, 
name));
     try {
       Tag tag = dispatcher.getTagForMetadataObject(metalake, metadataObject, 
name);
-      TagInfo tagInfo = new TagInfo(tag.name(), tag.comment(), 
tag.properties());
+      TagInfo tagInfo = tagInfo(tag);
       eventBus.dispatchEvent(
           new GetTagForMetadataObjectEvent(
               PrincipalUtils.getCurrentUserName(), metalake, metadataObject, 
tagInfo));
@@ -311,4 +379,27 @@ public class TagEventDispatcher implements TagDispatcher {
       throw e;
     }
   }
+
+  private static TagInfo tagInfo(Tag tag) {
+    return new TagInfo(
+        tag.name(),
+        tag.comment(),
+        tag.properties(),
+        allowedValuesForInfo(tag.valueConstraint()),
+        tag.assignment().map(assignment -> assignment.values()).orElse(null));
+  }
+
+  private static String[] allowedValuesForInfo(TagValueConstraint 
valueConstraint) {
+    TagValueConstraint normalizedConstraint =
+        valueConstraint == null ? TagValueConstraint.anyValue() : 
valueConstraint;
+    switch (normalizedConstraint.type()) {
+      case ANY_VALUE:
+        return null;
+      case NO_VALUE:
+      case ALLOWED_VALUES:
+        return normalizedConstraint.allowedValues();
+      default:
+        throw new IllegalArgumentException("Unknown tag value constraint: " + 
normalizedConstraint);
+    }
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectEvent.java
index 77480d743d..716c3e3196 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectEvent.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectEvent.java
@@ -19,8 +19,10 @@
 
 package org.apache.gravitino.listener.api.event;
 
+import java.util.Arrays;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.tag.TagValue;
 import org.apache.gravitino.utils.MetadataObjectUtil;
 
 /**
@@ -32,6 +34,8 @@ public final class AssociateTagsForMetadataObjectEvent 
extends TagEvent {
   private final String[] tagsToAdd;
   private final String[] tagsToRemove;
   private final String[] associatedTags;
+  private final TagValue[] tagValuesToAdd;
+  private final TagValue[] tagValuesToRemove;
 
   /**
    * Constructs an instance of {@code AssociateTagsForMetadataObjectEvent}.
@@ -50,11 +54,39 @@ public final class AssociateTagsForMetadataObjectEvent 
extends TagEvent {
       String[] tagsToAdd,
       String[] tagsToRemove,
       String[] associatedTags) {
+    this(
+        user,
+        metalake,
+        metadataObject,
+        tagValues(tagsToAdd),
+        tagValues(tagsToRemove),
+        associatedTags);
+  }
+
+  /**
+   * Constructs an instance of {@code AssociateTagsForMetadataObjectEvent}.
+   *
+   * @param user The username of the individual who initiated the tag 
association.
+   * @param metalake The metalake from which the tags were associated.
+   * @param metadataObject The metadata object with which the tags were 
associated.
+   * @param tagsToAdd The tag values that were added.
+   * @param tagsToRemove The tag values that were removed.
+   * @param associatedTags The resulting list of associated tags after the 
operation.
+   */
+  public AssociateTagsForMetadataObjectEvent(
+      String user,
+      String metalake,
+      MetadataObject metadataObject,
+      TagValue[] tagsToAdd,
+      TagValue[] tagsToRemove,
+      String[] associatedTags) {
     super(user, MetadataObjectUtil.toEntityIdent(metalake, metadataObject));
     this.objectType = metadataObject.type();
-    this.tagsToAdd = tagsToAdd != null ? tagsToAdd.clone() : new String[0];
-    this.tagsToRemove = tagsToRemove != null ? tagsToRemove.clone() : new 
String[0];
+    this.tagsToAdd = tagNames(tagsToAdd);
+    this.tagsToRemove = tagNames(tagsToRemove);
     this.associatedTags = associatedTags != null ? associatedTags.clone() : 
new String[0];
+    this.tagValuesToAdd = copyTagValues(tagsToAdd);
+    this.tagValuesToRemove = copyTagValues(tagsToRemove);
   }
 
   /**
@@ -72,7 +104,7 @@ public final class AssociateTagsForMetadataObjectEvent 
extends TagEvent {
    * @return An array of tag names that were added.
    */
   public String[] tagsToAdd() {
-    return tagsToAdd;
+    return tagsToAdd.clone();
   }
 
   /**
@@ -81,7 +113,25 @@ public final class AssociateTagsForMetadataObjectEvent 
extends TagEvent {
    * @return An array of tag names that were removed.
    */
   public String[] tagsToRemove() {
-    return tagsToRemove;
+    return tagsToRemove.clone();
+  }
+
+  /**
+   * Provides the tag values that were added in this operation.
+   *
+   * @return An array of tag values that were added.
+   */
+  public TagValue[] tagValuesToAdd() {
+    return tagValuesToAdd.clone();
+  }
+
+  /**
+   * Provides the tag values that were removed in this operation.
+   *
+   * @return An array of tag values that were removed.
+   */
+  public TagValue[] tagValuesToRemove() {
+    return tagValuesToRemove.clone();
   }
 
   /**
@@ -90,7 +140,7 @@ public final class AssociateTagsForMetadataObjectEvent 
extends TagEvent {
    * @return An array of tag names representing the associated tags.
    */
   public String[] associatedTags() {
-    return associatedTags;
+    return associatedTags.clone();
   }
 
   /**
@@ -102,4 +152,24 @@ public final class AssociateTagsForMetadataObjectEvent 
extends TagEvent {
   public OperationType operationType() {
     return OperationType.ASSOCIATE_TAGS_FOR_METADATA_OBJECT;
   }
+
+  private static TagValue[] tagValues(String[] tagNames) {
+    if (tagNames == null) {
+      return new TagValue[0];
+    }
+
+    return 
Arrays.stream(tagNames).map(TagValue::noValue).toArray(TagValue[]::new);
+  }
+
+  private static String[] tagNames(TagValue[] tagValues) {
+    if (tagValues == null) {
+      return new String[0];
+    }
+
+    return Arrays.stream(tagValues).map(TagValue::name).toArray(String[]::new);
+  }
+
+  private static TagValue[] copyTagValues(TagValue[] tagValues) {
+    return tagValues == null ? new TagValue[0] : tagValues.clone();
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectFailureEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectFailureEvent.java
index e981841383..3cb7943d18 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectFailureEvent.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectFailureEvent.java
@@ -18,8 +18,10 @@
  */
 package org.apache.gravitino.listener.api.event;
 
+import java.util.Arrays;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.tag.TagValue;
 import org.apache.gravitino.utils.MetadataObjectUtil;
 
 /**
@@ -31,6 +33,8 @@ public class AssociateTagsForMetadataObjectFailureEvent 
extends TagFailureEvent
   private final MetadataObject.Type objectType;
   private final String[] tagsToAdd;
   private final String[] tagsToRemove;
+  private final TagValue[] tagValuesToAdd;
+  private final TagValue[] tagValuesToRemove;
 
   /**
    * Constructs a new {@code AssociateTagsForMetadataObjectFailureEvent} 
instance.
@@ -50,10 +54,33 @@ public class AssociateTagsForMetadataObjectFailureEvent 
extends TagFailureEvent
       String[] tagsToAdd,
       String[] tagsToRemove,
       Exception exception) {
+    this(user, metalake, metadataObject, tagValues(tagsToAdd), 
tagValues(tagsToRemove), exception);
+  }
+
+  /**
+   * Constructs a new {@code AssociateTagsForMetadataObjectFailureEvent} 
instance.
+   *
+   * @param user The user who initiated the operation.
+   * @param metalake The metalake name where the metadata object resides.
+   * @param metadataObject The metadata object for which tags are being 
associated.
+   * @param tagsToAdd The tag values to add.
+   * @param tagsToRemove The tag values to remove.
+   * @param exception The exception encountered during the operation, 
providing insights into the
+   *     reasons behind the failure.
+   */
+  public AssociateTagsForMetadataObjectFailureEvent(
+      String user,
+      String metalake,
+      MetadataObject metadataObject,
+      TagValue[] tagsToAdd,
+      TagValue[] tagsToRemove,
+      Exception exception) {
     super(user, MetadataObjectUtil.toEntityIdent(metalake, metadataObject), 
exception);
     this.objectType = metadataObject.type();
-    this.tagsToAdd = tagsToAdd;
-    this.tagsToRemove = tagsToRemove;
+    this.tagsToAdd = tagNames(tagsToAdd);
+    this.tagsToRemove = tagNames(tagsToRemove);
+    this.tagValuesToAdd = copyTagValues(tagsToAdd);
+    this.tagValuesToRemove = copyTagValues(tagsToRemove);
   }
 
   /**
@@ -71,7 +98,7 @@ public class AssociateTagsForMetadataObjectFailureEvent 
extends TagFailureEvent
    * @return The tags to add.
    */
   public String[] tagsToAdd() {
-    return tagsToAdd;
+    return tagsToAdd.clone();
   }
 
   /**
@@ -80,7 +107,25 @@ public class AssociateTagsForMetadataObjectFailureEvent 
extends TagFailureEvent
    * @return The tags to remove.
    */
   public String[] tagsToRemove() {
-    return tagsToRemove;
+    return tagsToRemove.clone();
+  }
+
+  /**
+   * Returns the tag values to add.
+   *
+   * @return The tag values to add.
+   */
+  public TagValue[] tagValuesToAdd() {
+    return tagValuesToAdd.clone();
+  }
+
+  /**
+   * Returns the tag values to remove.
+   *
+   * @return The tag values to remove.
+   */
+  public TagValue[] tagValuesToRemove() {
+    return tagValuesToRemove.clone();
   }
 
   /**
@@ -92,4 +137,24 @@ public class AssociateTagsForMetadataObjectFailureEvent 
extends TagFailureEvent
   public OperationType operationType() {
     return OperationType.ASSOCIATE_TAGS_FOR_METADATA_OBJECT;
   }
+
+  private static TagValue[] tagValues(String[] tagNames) {
+    if (tagNames == null) {
+      return new TagValue[0];
+    }
+
+    return 
Arrays.stream(tagNames).map(TagValue::noValue).toArray(TagValue[]::new);
+  }
+
+  private static String[] tagNames(TagValue[] tagValues) {
+    if (tagValues == null) {
+      return new String[0];
+    }
+
+    return Arrays.stream(tagValues).map(TagValue::name).toArray(String[]::new);
+  }
+
+  private static TagValue[] copyTagValues(TagValue[] tagValues) {
+    return tagValues == null ? new TagValue[0] : tagValues.clone();
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectPreEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectPreEvent.java
index 8f21cc96b0..a2792bb4ac 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectPreEvent.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AssociateTagsForMetadataObjectPreEvent.java
@@ -18,8 +18,10 @@
  */
 package org.apache.gravitino.listener.api.event;
 
+import java.util.Arrays;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.tag.TagValue;
 import org.apache.gravitino.utils.MetadataObjectUtil;
 
 /** Represents an event triggered before associating tags with a specific 
metadata object. */
@@ -28,6 +30,8 @@ public class AssociateTagsForMetadataObjectPreEvent extends 
TagPreEvent {
   private final MetadataObject.Type objectType;
   private final String[] tagsToAdd;
   private final String[] tagsToRemove;
+  private final TagValue[] tagValuesToAdd;
+  private final TagValue[] tagValuesToRemove;
 
   /**
    * Constructs the pre-event with user, metalake, metadata object, tags to 
add, and tags to remove.
@@ -44,10 +48,30 @@ public class AssociateTagsForMetadataObjectPreEvent extends 
TagPreEvent {
       MetadataObject metadataObject,
       String[] tagsToAdd,
       String[] tagsToRemove) {
+    this(user, metalake, metadataObject, tagValues(tagsToAdd), 
tagValues(tagsToRemove));
+  }
+
+  /**
+   * Constructs the pre-event with user, metalake, metadata object, and tag 
values.
+   *
+   * @param user The user initiating the operation.
+   * @param metalake The metalake environment name.
+   * @param metadataObject The metadata object for which tags will be 
associated.
+   * @param tagsToAdd Tag values to be added to the metadata object.
+   * @param tagsToRemove Tag values to be removed from the metadata object.
+   */
+  public AssociateTagsForMetadataObjectPreEvent(
+      String user,
+      String metalake,
+      MetadataObject metadataObject,
+      TagValue[] tagsToAdd,
+      TagValue[] tagsToRemove) {
     super(user, MetadataObjectUtil.toEntityIdent(metalake, metadataObject));
     this.objectType = metadataObject.type();
-    this.tagsToAdd = tagsToAdd;
-    this.tagsToRemove = tagsToRemove;
+    this.tagsToAdd = tagNames(tagsToAdd);
+    this.tagsToRemove = tagNames(tagsToRemove);
+    this.tagValuesToAdd = copyTagValues(tagsToAdd);
+    this.tagValuesToRemove = copyTagValues(tagsToRemove);
   }
 
   /**
@@ -65,7 +89,7 @@ public class AssociateTagsForMetadataObjectPreEvent extends 
TagPreEvent {
    * @return Array of tag names to be added.
    */
   public String[] tagsToAdd() {
-    return tagsToAdd;
+    return tagsToAdd.clone();
   }
 
   /**
@@ -74,7 +98,25 @@ public class AssociateTagsForMetadataObjectPreEvent extends 
TagPreEvent {
    * @return Array of tag names to be removed.
    */
   public String[] tagsToRemove() {
-    return tagsToRemove;
+    return tagsToRemove.clone();
+  }
+
+  /**
+   * Returns the tag values to be added.
+   *
+   * @return Array of tag values to be added.
+   */
+  public TagValue[] tagValuesToAdd() {
+    return tagValuesToAdd.clone();
+  }
+
+  /**
+   * Returns the tag values to be removed.
+   *
+   * @return Array of tag values to be removed.
+   */
+  public TagValue[] tagValuesToRemove() {
+    return tagValuesToRemove.clone();
   }
 
   /**
@@ -86,4 +128,24 @@ public class AssociateTagsForMetadataObjectPreEvent extends 
TagPreEvent {
   public OperationType operationType() {
     return OperationType.ASSOCIATE_TAGS_FOR_METADATA_OBJECT;
   }
+
+  private static TagValue[] tagValues(String[] tagNames) {
+    if (tagNames == null) {
+      return new TagValue[0];
+    }
+
+    return 
Arrays.stream(tagNames).map(TagValue::noValue).toArray(TagValue[]::new);
+  }
+
+  private static String[] tagNames(TagValue[] tagValues) {
+    if (tagValues == null) {
+      return new String[0];
+    }
+
+    return Arrays.stream(tagValues).map(TagValue::name).toArray(String[]::new);
+  }
+
+  private static TagValue[] copyTagValues(TagValue[] tagValues) {
+    return tagValues == null ? new TagValue[0] : tagValues.clone();
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/info/TagInfo.java 
b/core/src/main/java/org/apache/gravitino/listener/api/info/TagInfo.java
index 81d6444ab5..ab86a98dbe 100644
--- a/core/src/main/java/org/apache/gravitino/listener/api/info/TagInfo.java
+++ b/core/src/main/java/org/apache/gravitino/listener/api/info/TagInfo.java
@@ -19,9 +19,13 @@
 package org.apache.gravitino.listener.api.info;
 
 import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
 import java.util.Map;
+import java.util.Optional;
 import javax.annotation.Nullable;
 import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.tag.TagAssignment;
+import org.apache.gravitino.tag.TagValueConstraint;
 
 /**
  * Provides access to metadata about a Tag instance, designed for use by event 
listeners. This class
@@ -33,6 +37,8 @@ public final class TagInfo {
   private final String name;
   @Nullable private final String comment;
   private final Map<String, String> properties;
+  @Nullable private final String[] allowedValues;
+  @Nullable private final String[] assignmentValues;
 
   /**
    * Directly constructs TagInfo with specified details.
@@ -42,9 +48,31 @@ public final class TagInfo {
    * @param properties A map of properties associated with the Tag.
    */
   public TagInfo(String name, String comment, Map<String, String> properties) {
+    this(name, comment, properties, null, null);
+  }
+
+  /**
+   * Directly constructs TagInfo with specified details.
+   *
+   * @param name The name of the Tag.
+   * @param comment An optional description for the Tag.
+   * @param properties A map of properties associated with the Tag.
+   * @param allowedValues The optional allowed assignment values of the Tag.
+   * @param assignmentValues The optional assignment values of the Tag.
+   */
+  public TagInfo(
+      String name,
+      String comment,
+      Map<String, String> properties,
+      @Nullable String[] allowedValues,
+      @Nullable String[] assignmentValues) {
     this.name = name;
     this.comment = comment;
     this.properties = properties == null ? ImmutableMap.of() : 
ImmutableMap.copyOf(properties);
+    this.allowedValues =
+        allowedValues == null ? null : Arrays.copyOf(allowedValues, 
allowedValues.length);
+    this.assignmentValues =
+        assignmentValues == null ? null : Arrays.copyOf(assignmentValues, 
assignmentValues.length);
   }
 
   /**
@@ -74,4 +102,34 @@ public final class TagInfo {
   public Map<String, String> properties() {
     return properties;
   }
+
+  /**
+   * Returns the assignment value constraint of the Tag.
+   *
+   * @return The assignment value constraint.
+   */
+  public TagValueConstraint valueConstraint() {
+    if (allowedValues == null) {
+      return TagValueConstraint.anyValue();
+    }
+    if (allowedValues.length == 0) {
+      return TagValueConstraint.noValue();
+    }
+    return TagValueConstraint.ofAllowedValues(allowedValues);
+  }
+
+  /**
+   * Returns the assignment values of the Tag in metadata-object tag events.
+   *
+   * @return The assignment in context, or empty when no assignment context 
exists.
+   */
+  public Optional<TagAssignment> assignment() {
+    if (assignmentValues == null) {
+      return Optional.empty();
+    }
+    if (assignmentValues.length == 0) {
+      return Optional.of(TagAssignment.noValue());
+    }
+    return Optional.of(TagAssignment.ofValues(assignmentValues));
+  }
 }
diff --git a/core/src/main/java/org/apache/gravitino/tag/TagDispatcher.java 
b/core/src/main/java/org/apache/gravitino/tag/TagDispatcher.java
index e48113dfdf..2a2dbbd7f0 100644
--- a/core/src/main/java/org/apache/gravitino/tag/TagDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/tag/TagDispatcher.java
@@ -18,7 +18,9 @@
  */
 package org.apache.gravitino.tag;
 
+import java.util.Arrays;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.exceptions.NoSuchTagException;
 import org.apache.gravitino.exceptions.TagAlreadyExistsException;
@@ -66,6 +68,25 @@ public interface TagDispatcher {
    */
   Tag createTag(String metalake, String name, String comment, Map<String, 
String> properties);
 
+  /**
+   * Create a new tag in the specified metalake with an assignment value 
constraint.
+   *
+   * @param metalake The name of the metalake.
+   * @param name The name of the tag.
+   * @param comment A comment for the new tag.
+   * @param properties The properties of the tag.
+   * @param valueConstraint The assignment value constraint of the tag.
+   * @return The created tag.
+   */
+  default Tag createTag(
+      String metalake,
+      String name,
+      String comment,
+      Map<String, String> properties,
+      TagValueConstraint valueConstraint) {
+    return createTag(metalake, name, comment, properties);
+  }
+
   /**
    * Alter an existing tag in the specified metalake
    *
@@ -96,6 +117,19 @@ public interface TagDispatcher {
    */
   MetadataObject[] listMetadataObjectsForTag(String metalake, String name);
 
+  /**
+   * List all metadata objects associated with the specified tag and exact 
assignment value.
+   *
+   * @param metalake The name of the metalake.
+   * @param name The name of the tag.
+   * @param value The exact assignment value to match, or null to return all 
objects for the tag.
+   * @return The array of metadata objects associated with the specified tag 
and value.
+   */
+  default MetadataObject[] listMetadataObjectsForTag(
+      String metalake, String name, @Nullable String value) {
+    return listMetadataObjectsForTag(metalake, name);
+  }
+
   /**
    * List all tag names associated with the specified metadata object.
    *
@@ -126,6 +160,32 @@ public interface TagDispatcher {
   String[] associateTagsForMetadataObject(
       String metalake, MetadataObject metadataObject, String[] tagsToAdd, 
String[] tagsToRemove);
 
+  /**
+   * Associate or disassociate tag values with the specified metadata object.
+   *
+   * @param metalake The name of the metalake.
+   * @param metadataObject The metadata object to update tags for.
+   * @param tagsToAdd Tag values to associate with the object.
+   * @param tagsToRemove Tag values to disassociate from the object.
+   * @return An array of updated tag names.
+   */
+  default String[] associateTagValuesForMetadataObject(
+      String metalake,
+      MetadataObject metadataObject,
+      TagValue[] tagsToAdd,
+      TagValue[] tagsToRemove) {
+    String[] tagNamesToAdd =
+        tagsToAdd == null
+            ? null
+            : 
Arrays.stream(tagsToAdd).map(TagValue::name).toArray(String[]::new);
+    String[] tagNamesToRemove =
+        tagsToRemove == null
+            ? null
+            : 
Arrays.stream(tagsToRemove).map(TagValue::name).toArray(String[]::new);
+    return associateTagsForMetadataObject(
+        metalake, metadataObject, tagNamesToAdd, tagNamesToRemove);
+  }
+
   /**
    * Retrieve a specific tag associated with the specified metadata object.
    *
diff --git a/core/src/main/java/org/apache/gravitino/tag/TagManager.java 
b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
index b99721eae1..3122dd9997 100644
--- a/core/src/main/java/org/apache/gravitino/tag/TagManager.java
+++ b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
@@ -27,14 +27,19 @@ import java.io.IOException;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import javax.annotation.Nullable;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityAlreadyExistsException;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.RelationEdgeTarget;
+import org.apache.gravitino.RelationQuery;
+import org.apache.gravitino.RelationUpdate;
 import org.apache.gravitino.SupportsRelationOperations;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
@@ -105,6 +110,16 @@ public class TagManager implements TagDispatcher {
 
   public Tag createTag(String metalake, String name, String comment, 
Map<String, String> properties)
       throws TagAlreadyExistsException {
+    return createTag(metalake, name, comment, properties, 
TagValueConstraint.anyValue());
+  }
+
+  public Tag createTag(
+      String metalake,
+      String name,
+      String comment,
+      Map<String, String> properties,
+      TagValueConstraint valueConstraint)
+      throws TagAlreadyExistsException {
     Map<String, String> tagProperties = properties == null ? 
Collections.emptyMap() : properties;
     checkMetalake(NameIdentifier.of(metalake), entityStore);
 
@@ -119,6 +134,7 @@ public class TagManager implements TagDispatcher {
                   .withNamespace(NamespaceUtil.ofTag(metalake))
                   .withComment(comment)
                   .withProperties(tagProperties)
+                  .withAllowedValues(allowedValuesForStorage(valueConstraint))
                   .withAuditInfo(
                       AuditInfo.builder()
                           
.withCreator(PrincipalUtils.getCurrentPrincipal().getName())
@@ -208,6 +224,12 @@ public class TagManager implements TagDispatcher {
 
   public MetadataObject[] listMetadataObjectsForTag(String metalake, String 
name)
       throws NoSuchTagException {
+    return listMetadataObjectsForTag(metalake, name, null);
+  }
+
+  @Override
+  public MetadataObject[] listMetadataObjectsForTag(
+      String metalake, String name, @Nullable String value) throws 
NoSuchTagException {
     NameIdentifier tagId = NameIdentifierUtil.ofTag(metalake, name);
     checkMetalake(NameIdentifier.of(metalake), entityStore);
     return TreeLockUtils.doWithTreeLock(
@@ -224,9 +246,12 @@ public class TagManager implements TagDispatcher {
                 entityStore
                     .relationOperations()
                     .listEntitiesByRelation(
-                        
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
-                        tagId,
-                        Entity.EntityType.TAG);
+                        RelationQuery.of(
+                            
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+                            tagId,
+                            Entity.EntityType.TAG,
+                            true,
+                            value));
             return MetadataObjectService.fromGenericEntities(entities)
                 .toArray(new MetadataObject[0]);
           } catch (IOException e) {
@@ -315,32 +340,38 @@ public class TagManager implements TagDispatcher {
   public String[] associateTagsForMetadataObject(
       String metalake, MetadataObject metadataObject, String[] tagsToAdd, 
String[] tagsToRemove)
       throws NoSuchMetadataObjectException, TagAlreadyAssociatedException {
+    return associateTagValuesForMetadataObject(
+        metalake, metadataObject, toNoValue(tagsToAdd), 
toNoValue(tagsToRemove));
+  }
+
+  @Override
+  public String[] associateTagValuesForMetadataObject(
+      String metalake, MetadataObject metadataObject, TagValue[] tagsToAdd, 
TagValue[] tagsToRemove)
+      throws NoSuchMetadataObjectException, TagAlreadyAssociatedException {
     Preconditions.checkArgument(
         
SUPPORTED_METADATA_OBJECT_TYPES_FOR_TAGS.contains(metadataObject.type()),
         "Cannot associate tags for unsupported metadata object type %s",
         metadataObject.type());
+    validateTagValuesToAdd(tagsToAdd);
+    validateTagValuesToRemove(tagsToRemove);
 
     NameIdentifier entityIdent = MetadataObjectUtil.toEntityIdent(metalake, 
metadataObject);
     Entity.EntityType entityType = 
MetadataObjectUtil.toEntityType(metadataObject);
 
     MetadataObjectUtil.checkMetadataObject(metalake, metadataObject);
 
-    // Remove all the tags that are both set to add and remove
-    Set<String> tagsToAddSet = tagsToAdd == null ? Sets.newHashSet() : 
Sets.newHashSet(tagsToAdd);
-    Set<String> tagsToRemoveSet =
-        tagsToRemove == null ? Sets.newHashSet() : 
Sets.newHashSet(tagsToRemove);
-    Set<String> common = Sets.intersection(tagsToAddSet, 
tagsToRemoveSet).immutableCopy();
+    Set<TagValue> tagsToAddSet =
+        tagsToAdd == null ? new LinkedHashSet<>() : new 
LinkedHashSet<>(Arrays.asList(tagsToAdd));
+    Set<TagValue> tagsToRemoveSet =
+        tagsToRemove == null
+            ? new LinkedHashSet<>()
+            : new LinkedHashSet<>(Arrays.asList(tagsToRemove));
+    Set<TagValue> common = Sets.intersection(tagsToAddSet, 
tagsToRemoveSet).immutableCopy();
     tagsToAddSet.removeAll(common);
     tagsToRemoveSet.removeAll(common);
 
-    NameIdentifier[] tagsToAddIdent =
-        tagsToAddSet.stream()
-            .map(tag -> NameIdentifierUtil.ofTag(metalake, tag))
-            .toArray(NameIdentifier[]::new);
-    NameIdentifier[] tagsToRemoveIdent =
-        tagsToRemoveSet.stream()
-            .map(tag -> NameIdentifierUtil.ofTag(metalake, tag))
-            .toArray(NameIdentifier[]::new);
+    TagValue[] tagValuesToAdd = tagsToAddSet.toArray(new TagValue[0]);
+    TagValue[] tagValuesToRemove = tagsToRemoveSet.toArray(new TagValue[0]);
 
     return TreeLockUtils.doWithTreeLock(
         entityIdent,
@@ -355,13 +386,14 @@ public class TagManager implements TagDispatcher {
                         entityStore
                             .relationOperations()
                             .updateEntityRelations(
-                                
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
-                                entityIdent,
-                                entityType,
-                                tagsToAddIdent,
-                                tagsToRemoveIdent);
-
-                    return tags.stream().map(Tag::name).toArray(String[]::new);
+                                RelationUpdate.of(
+                                    
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+                                    entityIdent,
+                                    entityType,
+                                    toRelationEdgeTargets(metalake, 
tagValuesToAdd),
+                                    toRelationEdgeTargets(metalake, 
tagValuesToRemove)));
+
+                    return 
tags.stream().map(Tag::name).distinct().toArray(String[]::new);
                   } catch (NoSuchEntityException e) {
                     throw new NoSuchMetadataObjectException(
                         e,
@@ -370,7 +402,7 @@ public class TagManager implements TagDispatcher {
                   } catch (EntityAlreadyExistsException e) {
                     throw new TagAlreadyAssociatedException(
                         e,
-                        "Failed to associate tags for metadata object due to 
some tags %s already "
+                        "Failed to associate tags for metadata object due to 
some tag values %s already "
                             + "associated to the metadata object %s",
                         Arrays.toString(tagsToAdd),
                         metadataObject);
@@ -381,6 +413,69 @@ public class TagManager implements TagDispatcher {
                 }));
   }
 
+  private static String[] allowedValuesForStorage(TagValueConstraint 
valueConstraint) {
+    TagValueConstraint normalizedConstraint =
+        valueConstraint == null ? TagValueConstraint.anyValue() : 
valueConstraint;
+    switch (normalizedConstraint.type()) {
+      case ANY_VALUE:
+        return null;
+      case NO_VALUE:
+        return normalizedConstraint.allowedValues();
+      case ALLOWED_VALUES:
+        return Arrays.stream(normalizedConstraint.allowedValues())
+            .distinct()
+            .toArray(String[]::new);
+      default:
+        throw new IllegalArgumentException("Unknown tag value constraint: " + 
normalizedConstraint);
+    }
+  }
+
+  private static void validateTagValuesToAdd(TagValue[] tagValues) {
+    if (tagValues == null) {
+      return;
+    }
+
+    Map<String, Boolean> valuedByTagName = Maps.newHashMap();
+    for (TagValue tagValue : tagValues) {
+      Preconditions.checkNotNull(tagValue, "Tag value to add must not be 
null");
+      boolean valued = tagValue.value().isPresent();
+      Boolean previous = valuedByTagName.putIfAbsent(tagValue.name(), valued);
+      Preconditions.checkArgument(
+          previous == null || previous == valued,
+          "Cannot add assignments both with and without values for tag %s",
+          tagValue.name());
+    }
+  }
+
+  private static void validateTagValuesToRemove(TagValue[] tagValues) {
+    if (tagValues == null) {
+      return;
+    }
+
+    for (TagValue tagValue : tagValues) {
+      Preconditions.checkNotNull(tagValue, "Tag value to remove must not be 
null");
+    }
+  }
+
+  private static TagValue[] toNoValue(String[] tags) {
+    if (tags == null) {
+      return null;
+    }
+
+    return Arrays.stream(tags).map(TagValue::noValue).toArray(TagValue[]::new);
+  }
+
+  private static RelationEdgeTarget[] toRelationEdgeTargets(String metalake, 
TagValue[] tagValues) {
+    return Arrays.stream(tagValues)
+        .map(
+            tagValue ->
+                RelationEdgeTarget.of(
+                    NameIdentifierUtil.ofTag(metalake, tagValue.name()),
+                    Entity.EntityType.TAG,
+                    tagValue.value().orElse(null)))
+        .toArray(RelationEdgeTarget[]::new);
+  }
+
   private TagEntity updateTagEntity(TagEntity tagEntity, TagChange... changes) 
{
     Map<String, String> props =
         tagEntity.properties() == null
@@ -411,6 +506,7 @@ public class TagManager implements TagDispatcher {
         .withNamespace(tagEntity.namespace())
         .withComment(newComment)
         .withProperties(props)
+        
.withAllowedValues(allowedValuesForStorage(tagEntity.valueConstraint()))
         .withAuditInfo(
             AuditInfo.builder()
                 .withCreator(tagEntity.auditInfo().creator())
diff --git 
a/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java 
b/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java
index 2819decf62..e83abefebf 100644
--- a/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java
+++ b/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java
@@ -62,7 +62,7 @@ public class TestTagHookDispatcher {
   @Test
   public void testCreateTagThrowsWhenSetOwnerFails() {
     Tag mockTag = mock(Tag.class);
-    when(mockDispatcher.createTag(any(), any(), any(), 
any())).thenReturn(mockTag);
+    when(mockDispatcher.createTag(any(), any(), any(), any(), 
any())).thenReturn(mockTag);
 
     doThrow(new RuntimeException("Set owner failed"))
         .when(mockOwnerDispatcher)
@@ -75,6 +75,6 @@ public class TestTagHookDispatcher {
                 hookDispatcher.createTag(
                     "test_metalake", "test_tag", "comment", 
Collections.emptyMap()));
     Assertions.assertEquals("Set owner failed", thrown.getMessage());
-    verify(mockDispatcher).createTag(any(), any(), any(), any());
+    verify(mockDispatcher).createTag(any(), any(), any(), any(), any());
   }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestTagEvent.java 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestTagEvent.java
index b68d4dd395..fd2b1245bf 100644
--- 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestTagEvent.java
+++ 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestTagEvent.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.listener.api.event;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -27,6 +28,7 @@ import com.google.common.collect.ImmutableMap;
 import java.util.Arrays;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
@@ -38,6 +40,8 @@ import org.apache.gravitino.listener.api.info.TagInfo;
 import org.apache.gravitino.tag.Tag;
 import org.apache.gravitino.tag.TagChange;
 import org.apache.gravitino.tag.TagDispatcher;
+import org.apache.gravitino.tag.TagValue;
+import org.apache.gravitino.tag.TagValueConstraint;
 import org.apache.gravitino.utils.MetadataObjectUtil;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.junit.jupiter.api.Assertions;
@@ -301,6 +305,27 @@ public class TestTagEvent {
     Assertions.assertEquals(OperationStatus.SUCCESS, 
postevent.operationStatus());
   }
 
+  @Test
+  void testAssociateTagValuesForMetadataObjectEvents() {
+    MetadataObject metadataObject =
+        NameIdentifierUtil.toMetadataObject(
+            NameIdentifierUtil.ofCatalog("metalake", "catalog_for_test"),
+            Entity.EntityType.CATALOG);
+    TagValue[] tagsToAdd = {TagValue.of("data_domain", "finance")};
+    TagValue[] tagsToRemove = {TagValue.noValue("pii")};
+    dispatcher.associateTagValuesForMetadataObject(
+        "metalake", metadataObject, tagsToAdd, tagsToRemove);
+
+    AssociateTagsForMetadataObjectPreEvent preEvent =
+        (AssociateTagsForMetadataObjectPreEvent) 
dummyEventListener.popPreEvent();
+    Assertions.assertArrayEquals(tagsToAdd, preEvent.tagValuesToAdd());
+    Assertions.assertArrayEquals(tagsToRemove, preEvent.tagValuesToRemove());
+    AssociateTagsForMetadataObjectEvent postEvent =
+        (AssociateTagsForMetadataObjectEvent) 
dummyEventListener.popPostEvent();
+    Assertions.assertArrayEquals(tagsToAdd, postEvent.tagValuesToAdd());
+    Assertions.assertArrayEquals(tagsToRemove, postEvent.tagValuesToRemove());
+  }
+
   @Test
   void testGetTagForMetadataObject() {
     MetadataObject metadataObject =
@@ -509,15 +534,35 @@ public class TestTagEvent {
     Assertions.assertEquals(
         MetadataObject.Type.CATALOG,
         ((AssociateTagsForMetadataObjectFailureEvent) event).objectType());
-    Assertions.assertEquals(
+    Assertions.assertArrayEquals(
         tagsToAssociate, ((AssociateTagsForMetadataObjectFailureEvent) 
event).tagsToAdd());
-    Assertions.assertEquals(
+    Assertions.assertArrayEquals(
         tagsToDisassociate, ((AssociateTagsForMetadataObjectFailureEvent) 
event).tagsToRemove());
     Assertions.assertEquals(
         OperationType.ASSOCIATE_TAGS_FOR_METADATA_OBJECT, 
event.operationType());
     Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
   }
 
+  @Test
+  void testAssociateTagValuesForMetadataObjectFailureEvent() {
+    MetadataObject metadataObject =
+        NameIdentifierUtil.toMetadataObject(
+            NameIdentifierUtil.ofCatalog("metalake", "catalog_for_test"),
+            Entity.EntityType.CATALOG);
+    TagValue[] tagsToAdd = {TagValue.of("data_domain", "finance")};
+    TagValue[] tagsToRemove = {TagValue.noValue("pii")};
+    Assertions.assertThrowsExactly(
+        GravitinoRuntimeException.class,
+        () ->
+            failureDispatcher.associateTagValuesForMetadataObject(
+                "metalake", metadataObject, tagsToAdd, tagsToRemove));
+
+    AssociateTagsForMetadataObjectFailureEvent event =
+        (AssociateTagsForMetadataObjectFailureEvent) 
dummyEventListener.popPostEvent();
+    Assertions.assertArrayEquals(tagsToAdd, event.tagValuesToAdd());
+    Assertions.assertArrayEquals(tagsToRemove, event.tagValuesToRemove());
+  }
+
   @Test
   @SuppressWarnings("deprecation")
   void testListMetadataObjectsForTagEventDeprecatedConstructor() {
@@ -555,6 +600,8 @@ public class TestTagEvent {
     when(tag.name()).thenReturn("tag");
     when(tag.comment()).thenReturn("comment");
     when(tag.properties()).thenReturn(ImmutableMap.of("color", "#FFFFFF"));
+    when(tag.valueConstraint()).thenReturn(TagValueConstraint.anyValue());
+    when(tag.assignment()).thenReturn(Optional.empty());
     return tag;
   }
 
@@ -570,6 +617,13 @@ public class TestTagEvent {
     Assertions.assertEquals(expectedTag.name(), actualTagInfo.name());
     Assertions.assertEquals(expectedTag.comment(), actualTagInfo.comment());
     Assertions.assertEquals(expectedTag.properties(), 
actualTagInfo.properties());
+    Assertions.assertEquals(
+        expectedTag.valueConstraint().type(), 
actualTagInfo.valueConstraint().type());
+    Assertions.assertArrayEquals(
+        expectedTag.valueConstraint().allowedValues(),
+        actualTagInfo.valueConstraint().allowedValues());
+    Assertions.assertEquals(
+        expectedTag.assignment().isPresent(), 
actualTagInfo.assignment().isPresent());
   }
 
   private TagDispatcher mockTagDispatcher() {
@@ -579,7 +633,11 @@ public class TestTagEvent {
     Tag[] tags = new Tag[] {tag, tag};
 
     when(dispatcher.createTag(
-            any(String.class), any(String.class), any(String.class), 
any(Map.class)))
+            any(String.class),
+            any(String.class),
+            any(String.class),
+            any(Map.class),
+            nullable(TagValueConstraint.class)))
         .thenReturn(tag);
     when(dispatcher.listTags(metalake)).thenReturn(tagNames);
     when(dispatcher.listTagsInfo(metalake)).thenReturn(tags);
diff --git a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java 
b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
index f5e28244ba..ff2b33046c 100644
--- a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
+++ b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
@@ -496,12 +496,14 @@ public class TestTagManager {
     Assertions.assertEquals(ImmutableSet.of("tag2", "tag3"), 
ImmutableSet.copyOf(tags1));
 
     // Test associate and disassociate no tags for catalog
-    String[] tags2 = tagManager.associateTagsForMetadataObject(METALAKE, 
catalogObject, null, null);
+    String[] tags2 =
+        tagManager.associateTagsForMetadataObject(
+            METALAKE, catalogObject, (String[]) null, (String[]) null);
 
     Assertions.assertEquals(2, tags2.length);
     Assertions.assertEquals(ImmutableSet.of("tag2", "tag3"), 
ImmutableSet.copyOf(tags2));
 
-    // Test re-associate tags for catalog
+    // Test re-associate tags for catalog through the compatibility API.
     Throwable e =
         Assertions.assertThrows(
             TagAlreadyAssociatedException.class,
@@ -511,12 +513,13 @@ public class TestTagManager {
     Assertions.assertTrue(e.getMessage().contains("Failed to associate tags 
for metadata object"));
 
     // Test associate and disassociate non-existent tags for catalog
-    String[] tags3 =
+    String[] tagsAfterMissingUpdate =
         tagManager.associateTagsForMetadataObject(
             METALAKE, catalogObject, new String[] {"tag4", "tag5"}, new 
String[] {"tag6"});
 
-    Assertions.assertEquals(2, tags3.length);
-    Assertions.assertEquals(ImmutableSet.of("tag2", "tag3"), 
ImmutableSet.copyOf(tags3));
+    Assertions.assertEquals(2, tagsAfterMissingUpdate.length);
+    Assertions.assertEquals(
+        ImmutableSet.of("tag2", "tag3"), 
ImmutableSet.copyOf(tagsAfterMissingUpdate));
 
     // Test associate tags for non-existent metadata object
     MetadataObject nonExistentObject =
@@ -644,6 +647,126 @@ public class TestTagManager {
     Assertions.assertEquals(ImmutableSet.of("tag3"), 
ImmutableSet.copyOf(tags9));
   }
 
+  @Test
+  public void testAssociateTagValuesForMetadataObject() {
+    Tag tag =
+        tagManager.createTag(
+            METALAKE,
+            "data_domain",
+            null,
+            null,
+            TagValueConstraint.ofAllowedValues("finance", "risk", "finance"));
+    MetadataObject tableObject =
+        NameIdentifierUtil.toMetadataObject(
+            NameIdentifierUtil.ofTable(METALAKE, CATALOG, SCHEMA, TABLE), 
Entity.EntityType.TABLE);
+
+    String[] tags =
+        tagManager.associateTagValuesForMetadataObject(
+            METALAKE,
+            tableObject,
+            new TagValue[] {TagValue.of(tag.name(), "finance"), 
TagValue.of(tag.name(), "risk")},
+            null);
+
+    Assertions.assertEquals(1, tags.length);
+    Assertions.assertEquals(ImmutableSet.of("data_domain"), 
ImmutableSet.copyOf(tags));
+
+    Tag[] tagInfos = tagManager.listTagsInfoForMetadataObject(METALAKE, 
tableObject);
+    Assertions.assertEquals(1, tagInfos.length);
+    Assertions.assertTrue(tagInfos[0].assignment().isPresent());
+    Assertions.assertArrayEquals(
+        new String[] {"finance", "risk"}, 
tagInfos[0].assignment().get().values());
+
+    String[] repeatedTags =
+        tagManager.associateTagValuesForMetadataObject(
+            METALAKE, tableObject, new TagValue[] {TagValue.of(tag.name(), 
"finance")}, null);
+    Assertions.assertArrayEquals(new String[] {tag.name()}, repeatedTags);
+    Tag[] repeatedTagInfos = 
tagManager.listTagsInfoForMetadataObject(METALAKE, tableObject);
+    Assertions.assertArrayEquals(
+        new String[] {"finance", "risk"}, 
repeatedTagInfos[0].assignment().get().values());
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            tagManager.associateTagValuesForMetadataObject(
+                METALAKE, tableObject, new TagValue[] {TagValue.of(tag.name(), 
"pii")}, null));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            tagManager.associateTagValuesForMetadataObject(
+                METALAKE, tableObject, new TagValue[] 
{TagValue.noValue(tag.name())}, null));
+
+    MetadataObject[] financeObjects =
+        tagManager.listMetadataObjectsForTag(METALAKE, tag.name(), "finance");
+    Assertions.assertArrayEquals(new MetadataObject[] {tableObject}, 
financeObjects);
+    Assertions.assertEquals(
+        0, tagManager.listMetadataObjectsForTag(METALAKE, tag.name(), 
"pii").length);
+
+    Tag ownerTag = tagManager.createTag(METALAKE, "owner", null, null);
+    tagManager.associateTagValuesForMetadataObject(
+        METALAKE, tableObject, new TagValue[] 
{TagValue.noValue(ownerTag.name())}, null);
+    Tag ownerInfoWithoutValue =
+        tagManager.getTagForMetadataObject(METALAKE, tableObject, 
ownerTag.name());
+    assertAssignmentValues(ownerInfoWithoutValue, new String[0]);
+    tagManager.associateTagValuesForMetadataObject(
+        METALAKE, tableObject, new TagValue[] {TagValue.of(ownerTag.name(), 
"team-a")}, null);
+    Tag ownerInfo = tagManager.getTagForMetadataObject(METALAKE, tableObject, 
ownerTag.name());
+    Assertions.assertArrayEquals(new String[] {"team-a"}, 
ownerInfo.assignment().get().values());
+    Assertions.assertArrayEquals(
+        new MetadataObject[] {tableObject},
+        tagManager.listMetadataObjectsForTag(METALAKE, ownerTag.name(), 
"team-a"));
+
+    Assertions.assertTrue(
+        tagInfos[0].valueConstraint().type() == 
TagValueConstraint.Type.ALLOWED_VALUES);
+    Assertions.assertArrayEquals(
+        new String[] {"finance", "risk"}, 
tagInfos[0].valueConstraint().allowedValues());
+  }
+
+  @Test
+  public void testRejectNullTagValueToRemove() {
+    MetadataObject tableObject =
+        NameIdentifierUtil.toMetadataObject(
+            NameIdentifierUtil.ofTable(METALAKE, CATALOG, SCHEMA, TABLE), 
Entity.EntityType.TABLE);
+
+    NullPointerException exception =
+        Assertions.assertThrows(
+            NullPointerException.class,
+            () ->
+                tagManager.associateTagValuesForMetadataObject(
+                    METALAKE, tableObject, null, new TagValue[] {null}));
+    Assertions.assertEquals("Tag value to remove must not be null", 
exception.getMessage());
+  }
+
+  @Test
+  public void testRejectMixedTagAdditionsWithAndWithoutValues() {
+    Tag noValueFirstTag = tagManager.createTag(METALAKE, "no_value_first", 
null, null);
+    Tag valuedFirstTag = tagManager.createTag(METALAKE, "valued_first", null, 
null);
+    MetadataObject tableObject =
+        NameIdentifierUtil.toMetadataObject(
+            NameIdentifierUtil.ofTable(METALAKE, CATALOG, SCHEMA, TABLE), 
Entity.EntityType.TABLE);
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            tagManager.associateTagValuesForMetadataObject(
+                METALAKE,
+                tableObject,
+                new TagValue[] {
+                  TagValue.noValue(noValueFirstTag.name()),
+                  TagValue.of(noValueFirstTag.name(), "finance")
+                },
+                null));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            tagManager.associateTagValuesForMetadataObject(
+                METALAKE,
+                tableObject,
+                new TagValue[] {
+                  TagValue.of(valuedFirstTag.name(), "finance"),
+                  TagValue.noValue(valuedFirstTag.name())
+                },
+                null));
+  }
+
   @Test
   public void testListMetadataObjectsForTag() {
     Tag tag1 = tagManager.createTag(METALAKE, "tag1", null, null);
@@ -757,7 +880,8 @@ public class TestTagManager {
 
     Tag[] tagsInfo = tagManager.listTagsInfoForMetadataObject(METALAKE, 
catalogObject);
     Assertions.assertEquals(3, tagsInfo.length);
-    Assertions.assertEquals(ImmutableSet.of(tag1, tag2, tag3), 
ImmutableSet.copyOf(tagsInfo));
+    Assertions.assertEquals(ImmutableSet.of("tag1", "tag2", "tag3"), 
tagNames(tagsInfo));
+    Arrays.stream(tagsInfo).forEach(tag -> assertAssignmentValues(tag, new 
String[0]));
 
     String[] tags1 = tagManager.listTagsForMetadataObject(METALAKE, 
schemaObject);
     Assertions.assertEquals(2, tags1.length);
@@ -765,7 +889,8 @@ public class TestTagManager {
 
     Tag[] tagsInfo1 = tagManager.listTagsInfoForMetadataObject(METALAKE, 
schemaObject);
     Assertions.assertEquals(2, tagsInfo1.length);
-    Assertions.assertEquals(ImmutableSet.of(tag1, tag2), 
ImmutableSet.copyOf(tagsInfo1));
+    Assertions.assertEquals(ImmutableSet.of("tag1", "tag2"), 
tagNames(tagsInfo1));
+    Arrays.stream(tagsInfo1).forEach(tag -> assertAssignmentValues(tag, new 
String[0]));
 
     String[] tags2 = tagManager.listTagsForMetadataObject(METALAKE, 
tableObject);
     Assertions.assertEquals(1, tags2.length);
@@ -773,7 +898,8 @@ public class TestTagManager {
 
     Tag[] tagsInfo2 = tagManager.listTagsInfoForMetadataObject(METALAKE, 
tableObject);
     Assertions.assertEquals(1, tagsInfo2.length);
-    Assertions.assertEquals(ImmutableSet.of(tag1), 
ImmutableSet.copyOf(tagsInfo2));
+    Assertions.assertEquals(ImmutableSet.of("tag1"), tagNames(tagsInfo2));
+    Arrays.stream(tagsInfo2).forEach(tag -> assertAssignmentValues(tag, new 
String[0]));
 
     String[] tags3 = tagManager.listTagsForMetadataObject(METALAKE, 
columnObject);
     Assertions.assertEquals(1, tags3.length);
@@ -781,7 +907,8 @@ public class TestTagManager {
 
     Tag[] tagsInfo3 = tagManager.listTagsInfoForMetadataObject(METALAKE, 
columnObject);
     Assertions.assertEquals(1, tagsInfo3.length);
-    Assertions.assertEquals(ImmutableSet.of(tag1), 
ImmutableSet.copyOf(tagsInfo3));
+    Assertions.assertEquals(ImmutableSet.of("tag1"), tagNames(tagsInfo3));
+    Arrays.stream(tagsInfo3).forEach(tag -> assertAssignmentValues(tag, new 
String[0]));
 
     String[] tags4 = tagManager.listTagsForMetadataObject(METALAKE, 
viewObject);
     Assertions.assertEquals(1, tags4.length);
@@ -789,7 +916,8 @@ public class TestTagManager {
 
     Tag[] tagsInfo4 = tagManager.listTagsInfoForMetadataObject(METALAKE, 
viewObject);
     Assertions.assertEquals(1, tagsInfo4.length);
-    Assertions.assertEquals(ImmutableSet.of(tag1), 
ImmutableSet.copyOf(tagsInfo4));
+    Assertions.assertEquals(ImmutableSet.of("tag1"), tagNames(tagsInfo4));
+    Arrays.stream(tagsInfo4).forEach(tag -> assertAssignmentValues(tag, new 
String[0]));
 
     String[] tags5 = tagManager.listTagsForMetadataObject(METALAKE, 
functionObject);
     Assertions.assertEquals(1, tags5.length);
@@ -797,7 +925,8 @@ public class TestTagManager {
 
     Tag[] tagsInfo5 = tagManager.listTagsInfoForMetadataObject(METALAKE, 
functionObject);
     Assertions.assertEquals(1, tagsInfo5.length);
-    Assertions.assertEquals(ImmutableSet.of(tag1), 
ImmutableSet.copyOf(tagsInfo5));
+    Assertions.assertEquals(ImmutableSet.of("tag1"), tagNames(tagsInfo5));
+    Arrays.stream(tagsInfo5).forEach(tag -> assertAssignmentValues(tag, new 
String[0]));
 
     // List tags for non-existent metadata object
     MetadataObject nonExistentObject =
@@ -853,25 +982,32 @@ public class TestTagManager {
         METALAKE, functionObject, new String[] {tag1.name()}, null);
 
     Tag result = tagManager.getTagForMetadataObject(METALAKE, catalogObject, 
tag1.name());
-    Assertions.assertEquals(tag1, result);
+    assertTagMetadataEquals(tag1, result);
+    assertAssignmentValues(result, new String[0]);
 
     Tag result1 = tagManager.getTagForMetadataObject(METALAKE, schemaObject, 
tag1.name());
-    Assertions.assertEquals(tag1, result1);
+    assertTagMetadataEquals(tag1, result1);
+    assertAssignmentValues(result1, new String[0]);
 
     Tag result2 = tagManager.getTagForMetadataObject(METALAKE, schemaObject, 
tag2.name());
-    Assertions.assertEquals(tag2, result2);
+    assertTagMetadataEquals(tag2, result2);
+    assertAssignmentValues(result2, new String[0]);
 
     Tag result3 = tagManager.getTagForMetadataObject(METALAKE, catalogObject, 
tag3.name());
-    Assertions.assertEquals(tag3, result3);
+    assertTagMetadataEquals(tag3, result3);
+    assertAssignmentValues(result3, new String[0]);
 
     Tag result4 = tagManager.getTagForMetadataObject(METALAKE, tableObject, 
tag1.name());
-    Assertions.assertEquals(tag1, result4);
+    assertTagMetadataEquals(tag1, result4);
+    assertAssignmentValues(result4, new String[0]);
 
     Tag result5 = tagManager.getTagForMetadataObject(METALAKE, viewObject, 
tag1.name());
-    Assertions.assertEquals(tag1, result5);
+    assertTagMetadataEquals(tag1, result5);
+    assertAssignmentValues(result5, new String[0]);
 
     Tag result6 = tagManager.getTagForMetadataObject(METALAKE, functionObject, 
tag1.name());
-    Assertions.assertEquals(tag1, result6);
+    assertTagMetadataEquals(tag1, result6);
+    assertAssignmentValues(result6, new String[0]);
 
     // Test get non-existent tag for metadata object
     Throwable e =
@@ -904,4 +1040,22 @@ public class TestTagManager {
     Assertions.assertTrue(
         e3.getMessage().contains("Failed to get tag for metadata object " + 
nonExistentObject));
   }
+
+  private static Set<String> tagNames(Tag[] tags) {
+    return Arrays.stream(tags).map(Tag::name).collect(Collectors.toSet());
+  }
+
+  private static void assertTagMetadataEquals(Tag expected, Tag actual) {
+    Assertions.assertEquals(expected.name(), actual.name());
+    Assertions.assertEquals(expected.comment(), actual.comment());
+    Assertions.assertEquals(expected.properties(), actual.properties());
+    Assertions.assertEquals(expected.valueConstraint().type(), 
actual.valueConstraint().type());
+    Assertions.assertArrayEquals(
+        expected.valueConstraint().allowedValues(), 
actual.valueConstraint().allowedValues());
+  }
+
+  private static void assertAssignmentValues(Tag tag, String[] expectedValues) 
{
+    Assertions.assertTrue(tag.assignment().isPresent());
+    Assertions.assertArrayEquals(expectedValues, 
tag.assignment().get().values());
+  }
 }

Reply via email to