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

jshao 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 71e6651e2 [#3914] feat(server): Add REST server interface for Tag 
System (#3943)
71e6651e2 is described below

commit 71e6651e2bc7359a9c9b694fa7275871e4e11969
Author: Jerry Shao <jerrys...@datastrato.com>
AuthorDate: Mon Jul 22 20:48:52 2024 +0800

    [#3914] feat(server): Add REST server interface for Tag System (#3943)
    
    ### What changes were proposed in this pull request?
    
    This PR proposes to add REST server interface for Tag System
    
    ### Why are the changes needed?
    
    This is a part of work for Tag system.
    
    Fix: #3914
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes
    
    ### How was this patch tested?
    
    UTs added.
    
    ---------
    
    Co-authored-by: bknbkn <67318028+bkn...@users.noreply.github.com>
    Co-authored-by: Dev Parikh <51128342+dev79...@users.noreply.github.com>
    Co-authored-by: roryqi <ror...@apache.org>
    Co-authored-by: JinsYin <jins...@163.com>
    Co-authored-by: rqyin <rq...@easipass.com>
---
 .../java/org/apache/gravitino/MetadataObjects.java |   41 +
 .../gravitino/client/ObjectMapperProvider.java     |    4 +-
 .../gravitino/dto/requests/TagCreateRequest.java   |   76 ++
 .../gravitino/dto/requests/TagUpdateRequest.java   |  201 ++++
 .../gravitino/dto/requests/TagUpdatesRequest.java  |   57 +
 .../dto/requests/TagsAssociateRequest.java         |   82 ++
 .../dto/responses/MetadataObjectListResponse.java  |   73 ++
 .../gravitino/dto/responses/NameListResponse.java  |   65 ++
 .../gravitino/dto/responses/TagListResponse.java   |   65 ++
 .../gravitino/dto/responses/TagResponse.java       |   62 ++
 .../gravitino/dto/tag/MetadataObjectDTO.java       |  124 +++
 .../java/org/apache/gravitino/dto/tag/TagDTO.java  |  147 +++
 .../apache/gravitino/dto/util/DTOConverters.java   |   38 +
 .../java/org/apache/gravitino/json/JsonUtils.java  |    7 +-
 .../dto/requests/TestTagCreateRequest.java         |   49 +
 .../dto/requests/TestTagUpdatesRequest.java        |   91 ++
 .../gravitino/dto/responses/TestResponses.java     |   57 +
 .../gravitino/dto/tag/TestMetadataObjectDTO.java   |  150 +++
 .../org/apache/gravitino/dto/tag/TestTagDTO.java   |  103 ++
 .../gravitino/tag/SupportsTagOperations.java       |    1 -
 .../java/org/apache/gravitino/tag/TagManager.java  |    7 +-
 .../apache/gravitino/server/GravitinoServer.java   |    3 +-
 .../gravitino/server/web/ObjectMapperProvider.java |    4 +-
 .../server/web/rest/ExceptionHandlers.java         |   42 +
 .../gravitino/server/web/rest/OperationType.java   |    3 +-
 .../gravitino/server/web/rest/TagOperations.java   |  451 ++++++++
 .../server/web/rest/TestTagOperations.java         | 1099 ++++++++++++++++++++
 27 files changed, 3093 insertions(+), 9 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/MetadataObjects.java 
b/api/src/main/java/org/apache/gravitino/MetadataObjects.java
index 5136164c9..6bd72137e 100644
--- a/api/src/main/java/org/apache/gravitino/MetadataObjects.java
+++ b/api/src/main/java/org/apache/gravitino/MetadataObjects.java
@@ -22,6 +22,7 @@ import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 
 /** The helper class for {@link MetadataObject}. */
@@ -94,6 +95,46 @@ public class MetadataObjects {
     return new MetadataObjectImpl(getParentFullName(names), 
getLastName(names), type);
   }
 
+  /**
+   * Get the parent metadata object of the given metadata object.
+   *
+   * @param object The metadata object
+   * @return The parent metadata object if it exists, otherwise null
+   */
+  @Nullable
+  public static MetadataObject parent(MetadataObject object) {
+    if (object == null) {
+      return null;
+    }
+
+    // Return null if the object is the root object
+    if (object.type() == MetadataObject.Type.METALAKE
+        || object.type() == MetadataObject.Type.CATALOG) {
+      return null;
+    }
+
+    MetadataObject.Type parentType;
+    switch (object.type()) {
+      case COLUMN:
+        parentType = MetadataObject.Type.TABLE;
+        break;
+      case TABLE:
+      case FILESET:
+      case TOPIC:
+        parentType = MetadataObject.Type.SCHEMA;
+        break;
+      case SCHEMA:
+        parentType = MetadataObject.Type.CATALOG;
+        break;
+
+      default:
+        throw new IllegalArgumentException(
+            "Unexpected to reach here for metadata object type: " + 
object.type());
+    }
+
+    return parse(object.parent(), parentType);
+  }
+
   /**
    * Parse the metadata object with the given full name and type.
    *
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
index 1a906c581..44aab1b63 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.cfg.EnumFeature;
 import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 
 /**
@@ -39,7 +40,8 @@ public class ObjectMapperProvider {
             .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
             .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
             .build()
-            .registerModule(new JavaTimeModule());
+            .registerModule(new JavaTimeModule())
+            .registerModule(new Jdk8Module());
   }
 
   /**
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
new file mode 100644
index 000000000..5b786f47c
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.Map;
+import javax.annotation.Nullable;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to create a tag. */
+@Getter
+@EqualsAndHashCode
+@ToString
+public class TagCreateRequest implements RESTRequest {
+
+  @JsonProperty("name")
+  private final String name;
+
+  @JsonProperty("comment")
+  @Nullable
+  private final String comment;
+
+  @JsonProperty("properties")
+  @Nullable
+  private Map<String, String> properties;
+
+  /**
+   * 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.
+   */
+  public TagCreateRequest(String name, String comment, Map<String, String> 
properties) {
+    this.name = name;
+    this.comment = comment;
+    this.properties = properties;
+  }
+
+  /** This is the constructor that is used by Jackson deserializer */
+  public TagCreateRequest() {
+    this(null, null, null);
+  }
+
+  /**
+   * Validates the request.
+   *
+   * @throws IllegalArgumentException If the request is invalid, this 
exception is thrown.
+   */
+  @Override
+  public void validate() throws IllegalArgumentException {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(name), "\"name\" is required and cannot be 
empty");
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdateRequest.java 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdateRequest.java
new file mode 100644
index 000000000..5323ac565
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdateRequest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.tag.TagChange;
+
+/** Represents a request to update a tag. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
+@JsonSubTypes({
+  @JsonSubTypes.Type(value = TagUpdateRequest.RenameTagRequest.class, name = 
"rename"),
+  @JsonSubTypes.Type(
+      value = TagUpdateRequest.UpdateTagCommentRequest.class,
+      name = "updateComment"),
+  @JsonSubTypes.Type(value = TagUpdateRequest.SetTagPropertyRequest.class, 
name = "setProperty"),
+  @JsonSubTypes.Type(
+      value = TagUpdateRequest.RemoveTagPropertyRequest.class,
+      name = "removeProperty")
+})
+public interface TagUpdateRequest extends RESTRequest {
+
+  /**
+   * Returns the tag change.
+   *
+   * @return the tag change.
+   */
+  TagChange tagChange();
+
+  /** The tag update request for renaming a tag. */
+  @EqualsAndHashCode
+  @ToString
+  class RenameTagRequest implements TagUpdateRequest {
+
+    @Getter
+    @JsonProperty("newName")
+    private final String newName;
+
+    /**
+     * Creates a new RenameTagRequest.
+     *
+     * @param newName The new name of the tag.
+     */
+    public RenameTagRequest(String newName) {
+      this.newName = newName;
+    }
+
+    /** This is the constructor that is used by Jackson deserializer */
+    public RenameTagRequest() {
+      this.newName = null;
+    }
+
+    @Override
+    public TagChange tagChange() {
+      return TagChange.rename(newName);
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(StringUtils.isNotBlank(newName), 
"\"newName\" must not be blank");
+    }
+  }
+
+  /** The tag update request for updating a tag comment. */
+  @EqualsAndHashCode
+  @ToString
+  class UpdateTagCommentRequest implements TagUpdateRequest {
+
+    @Getter
+    @JsonProperty("newComment")
+    private final String newComment;
+
+    /**
+     * Creates a new UpdateTagCommentRequest.
+     *
+     * @param newComment The new comment of the tag.
+     */
+    public UpdateTagCommentRequest(String newComment) {
+      this.newComment = newComment;
+    }
+
+    /** This is the constructor that is used by Jackson deserializer */
+    public UpdateTagCommentRequest() {
+      this.newComment = null;
+    }
+
+    @Override
+    public TagChange tagChange() {
+      return TagChange.updateComment(newComment);
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(newComment), "\"newComment\" must not be 
blank");
+    }
+  }
+
+  /** The tag update request for setting a tag property. */
+  @EqualsAndHashCode
+  @ToString
+  class SetTagPropertyRequest implements TagUpdateRequest {
+
+    @Getter
+    @JsonProperty("property")
+    private final String property;
+
+    @Getter
+    @JsonProperty("value")
+    private final String value;
+
+    /**
+     * Creates a new SetTagPropertyRequest.
+     *
+     * @param property The property to set.
+     * @param value The value of the property.
+     */
+    public SetTagPropertyRequest(String property, String value) {
+      this.property = property;
+      this.value = value;
+    }
+
+    /** This is the constructor that is used by Jackson deserializer */
+    public SetTagPropertyRequest() {
+      this.property = null;
+      this.value = null;
+    }
+
+    @Override
+    public TagChange tagChange() {
+      return TagChange.setProperty(property, value);
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(property), "\"property\" must not be blank");
+      Preconditions.checkArgument(StringUtils.isNotBlank(value), "\"value\" 
must not be blank");
+    }
+  }
+
+  /** The tag update request for removing a tag property. */
+  @EqualsAndHashCode
+  @ToString
+  class RemoveTagPropertyRequest implements TagUpdateRequest {
+
+    @Getter
+    @JsonProperty("property")
+    private final String property;
+
+    /**
+     * Creates a new RemoveTagPropertyRequest.
+     *
+     * @param property The property to remove.
+     */
+    public RemoveTagPropertyRequest(String property) {
+      this.property = property;
+    }
+
+    /** This is the constructor that is used by Jackson deserializer */
+    public RemoveTagPropertyRequest() {
+      this.property = null;
+    }
+
+    @Override
+    public TagChange tagChange() {
+      return TagChange.removeProperty(property);
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(property), "\"property\" must not be blank");
+    }
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdatesRequest.java 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdatesRequest.java
new file mode 100644
index 000000000..4e878b0f7
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdatesRequest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.List;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to update a tag. */
+@Getter
+@EqualsAndHashCode
+@ToString
+public class TagUpdatesRequest implements RESTRequest {
+
+  @JsonProperty("updates")
+  private final List<TagUpdateRequest> updates;
+
+  /**
+   * Creates a new TagUpdatesRequest.
+   *
+   * @param updates The updates to apply to the tag.
+   */
+  public TagUpdatesRequest(List<TagUpdateRequest> updates) {
+    this.updates = updates;
+  }
+
+  /** This is the constructor that is used by Jackson deserializer */
+  public TagUpdatesRequest() {
+    this(null);
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    Preconditions.checkArgument(updates != null, "updates must not be null");
+    updates.forEach(RESTRequest::validate);
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/TagsAssociateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagsAssociateRequest.java
new file mode 100644
index 000000000..fe202b1d6
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/TagsAssociateRequest.java
@@ -0,0 +1,82 @@
+/*
+ * 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 lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to associate tags. */
+@Getter
+@EqualsAndHashCode
+@ToString
+public class TagsAssociateRequest implements RESTRequest {
+
+  @JsonProperty("tagsToAdd")
+  private final String[] tagsToAdd;
+
+  @JsonProperty("tagsToRemove")
+  private final String[] tagsToRemove;
+
+  /**
+   * Creates a new TagsAssociateRequest.
+   *
+   * @param tagsToAdd The tags to add.
+   * @param tagsToRemove The tags to remove.
+   */
+  public TagsAssociateRequest(String[] tagsToAdd, String[] tagsToRemove) {
+    this.tagsToAdd = tagsToAdd;
+    this.tagsToRemove = tagsToRemove;
+  }
+
+  /** This is the constructor that is used by Jackson deserializer */
+  public TagsAssociateRequest() {
+    this(null, null);
+  }
+
+  /**
+   * 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");
+
+    if (tagsToAdd != null) {
+      for (String tag : tagsToAdd) {
+        Preconditions.checkArgument(
+            StringUtils.isNotBlank(tag), "tagsToAdd must not contain null or 
empty tag names");
+      }
+    }
+
+    if (tagsToRemove != null) {
+      for (String tag : tagsToRemove) {
+        Preconditions.checkArgument(
+            StringUtils.isNotBlank(tag), "tagsToRemove must not contain null 
or empty tag names");
+      }
+    }
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/MetadataObjectListResponse.java
 
b/common/src/main/java/org/apache/gravitino/dto/responses/MetadataObjectListResponse.java
new file mode 100644
index 000000000..67b26a5f5
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/responses/MetadataObjectListResponse.java
@@ -0,0 +1,73 @@
+/*
+ * 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.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+
+/** Represents a response containing a list of metadata objects. */
+@Getter
+@EqualsAndHashCode(callSuper = true)
+@ToString
+public class MetadataObjectListResponse extends BaseResponse {
+
+  @JsonProperty("metadataObjects")
+  private final MetadataObjectDTO[] metadataObjects;
+
+  /**
+   * Constructor for MetadataObjectListResponse.
+   *
+   * @param metadataObjects The array of metadata object DTOs.
+   */
+  public MetadataObjectListResponse(MetadataObjectDTO[] metadataObjects) {
+    super(0);
+    this.metadataObjects = metadataObjects;
+  }
+
+  /** Default constructor for MetadataObjectListResponse. (Used for Jackson 
deserialization.) */
+  public MetadataObjectListResponse() {
+    super();
+    this.metadataObjects = null;
+  }
+
+  /**
+   * Validates the response data.
+   *
+   * @throws IllegalArgumentException if name or audit information is not set.
+   */
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+
+    Preconditions.checkArgument(metadataObjects != null, "metadataObjects must 
be non-null");
+    Arrays.stream(metadataObjects)
+        .forEach(
+            object ->
+                Preconditions.checkArgument(
+                    object != null
+                        && StringUtils.isNotBlank(object.name())
+                        && object.type() != null,
+                    "metadataObject must not be null and it's field cannot 
null or empty"));
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java 
b/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java
new file mode 100644
index 000000000..0728165c5
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+
+/** Represents a response for a list of entity names. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class NameListResponse extends BaseResponse {
+
+  @JsonProperty("names")
+  private final String[] names;
+
+  /**
+   * Creates a new NameListResponse.
+   *
+   * @param names The list of names.
+   */
+  public NameListResponse(String[] names) {
+    this.names = names;
+  }
+
+  /**
+   * This is the constructor that is used by Jackson deserializer to create an 
instance of
+   * NameListResponse.
+   */
+  public NameListResponse() {
+    this.names = null;
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+
+    Preconditions.checkArgument(names != null, "\"names\" must not be null");
+    Arrays.stream(names)
+        .forEach(
+            name ->
+                Preconditions.checkArgument(StringUtils.isNotBlank(name), 
"name must not be null"));
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/TagListResponse.java 
b/common/src/main/java/org/apache/gravitino/dto/responses/TagListResponse.java
new file mode 100644
index 000000000..93c324dfe
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/responses/TagListResponse.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.dto.tag.TagDTO;
+
+/** Represents a response for a list of tags. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class TagListResponse extends BaseResponse {
+
+  @JsonProperty("tags")
+  private final TagDTO[] tags;
+
+  /**
+   * Creates a new TagListResponse.
+   *
+   * @param tags The list of tags.
+   */
+  public TagListResponse(TagDTO[] tags) {
+    super(0);
+    this.tags = tags;
+  }
+
+  /**
+   * This is the constructor that is used by Jackson deserializer to create an 
instance of
+   * TagListResponse.
+   */
+  public TagListResponse() {
+    super();
+    this.tags = null;
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+
+    Preconditions.checkArgument(tags != null, "\"tags\" must not be null");
+    Arrays.stream(tags)
+        .forEach(t -> Preconditions.checkArgument(t != null, "tag must not be 
null"));
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/TagResponse.java 
b/common/src/main/java/org/apache/gravitino/dto/responses/TagResponse.java
new file mode 100644
index 000000000..9f154e4ed
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/responses/TagResponse.java
@@ -0,0 +1,62 @@
+/*
+ * 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.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.dto.tag.TagDTO;
+
+/** Represents a response for a tag. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class TagResponse extends BaseResponse {
+
+  @JsonProperty("tag")
+  private final TagDTO tag;
+
+  /**
+   * Creates a new TagResponse.
+   *
+   * @param tag The tag.
+   */
+  public TagResponse(TagDTO tag) {
+    super(0);
+    this.tag = tag;
+  }
+
+  /**
+   * This is the constructor that is used by Jackson deserializer to create an 
instance of
+   * TagResponse.
+   */
+  public TagResponse() {
+    super();
+    this.tag = null;
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+
+    Preconditions.checkArgument(tag != null, "\"tag\" must not be null");
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/tag/MetadataObjectDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/tag/MetadataObjectDTO.java
new file mode 100644
index 000000000..d09815b9e
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/tag/MetadataObjectDTO.java
@@ -0,0 +1,124 @@
+/*
+ * 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.tag;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.EqualsAndHashCode;
+import org.apache.gravitino.MetadataObject;
+
+/** Represents a Metadata Object DTO (Data Transfer Object). */
+@EqualsAndHashCode
+public class MetadataObjectDTO implements MetadataObject {
+
+  private String parent;
+
+  private String name;
+
+  @JsonProperty("type")
+  private Type type;
+
+  private MetadataObjectDTO() {}
+
+  @Override
+  public String parent() {
+    return parent;
+  }
+
+  @Override
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public Type type() {
+    return type;
+  }
+
+  /** @return The full name of the metadata object. */
+  @JsonProperty("fullName")
+  public String getFullName() {
+    return fullName();
+  }
+
+  /**
+   * Sets the full name of the metadata object. Only used by Jackson 
deserializer.
+   *
+   * @param fullName The full name of the metadata object.
+   */
+  @JsonProperty("fullName")
+  public void setFullName(String fullName) {
+    int index = fullName.lastIndexOf(".");
+    if (index == -1) {
+      parent = null;
+      name = fullName;
+    } else {
+      parent = fullName.substring(0, index);
+      name = fullName.substring(index + 1);
+    }
+  }
+
+  /** @return a new builder for constructing a Metadata Object DTO. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for constructing a Metadata Object DTO. */
+  public static class Builder {
+
+    private final MetadataObjectDTO metadataObjectDTO = new 
MetadataObjectDTO();
+
+    /**
+     * Sets the parent of the metadata object.
+     *
+     * @param parent The parent of the metadata object.
+     * @return The builder.
+     */
+    public Builder withParent(String parent) {
+      metadataObjectDTO.parent = parent;
+      return this;
+    }
+
+    /**
+     * Sets the name of the metadata object.
+     *
+     * @param name The name of the metadata object.
+     * @return The builder.
+     */
+    public Builder withName(String name) {
+      metadataObjectDTO.name = name;
+      return this;
+    }
+
+    /**
+     * Sets the type of the metadata object.
+     *
+     * @param type The type of the metadata object.
+     * @return The builder.
+     */
+    public Builder withType(Type type) {
+      metadataObjectDTO.type = type;
+      return this;
+    }
+
+    /** @return The constructed Metadata Object DTO. */
+    public MetadataObjectDTO build() {
+      return metadataObjectDTO;
+    }
+  }
+}
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
new file mode 100644
index 000000000..a7f25480b
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
@@ -0,0 +1,147 @@
+/*
+ * 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.tag;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+import java.util.Optional;
+import lombok.EqualsAndHashCode;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.tag.Tag;
+
+/** Represents a Tag Data Transfer Object (DTO). */
+@EqualsAndHashCode
+public class TagDTO implements Tag {
+
+  @JsonProperty("name")
+  private String name;
+
+  @JsonProperty("comment")
+  private String comment;
+
+  @JsonProperty("properties")
+  private Map<String, String> properties;
+
+  @JsonProperty("audit")
+  private AuditDTO audit;
+
+  @JsonProperty("inherited")
+  private Optional<Boolean> inherited = Optional.empty();
+
+  private TagDTO() {}
+
+  @Override
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public String comment() {
+    return comment;
+  }
+
+  @Override
+  public Map<String, String> properties() {
+    return properties;
+  }
+
+  @Override
+  public AuditDTO auditInfo() {
+    return audit;
+  }
+
+  @Override
+  public Optional<Boolean> inherited() {
+    return inherited;
+  }
+
+  /** @return a new builder for constructing a Tag DTO. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder class for constructing TagDTO instances. */
+  public static class Builder {
+    private final TagDTO tagDTO;
+
+    private Builder() {
+      tagDTO = new TagDTO();
+    }
+
+    /**
+     * Sets the name of the tag.
+     *
+     * @param name The name of the tag.
+     * @return The builder instance.
+     */
+    public Builder withName(String name) {
+      tagDTO.name = name;
+      return this;
+    }
+
+    /**
+     * Sets the comment associated with the tag.
+     *
+     * @param comment The comment associated with the tag.
+     * @return The builder instance.
+     */
+    public Builder withComment(String comment) {
+      tagDTO.comment = comment;
+      return this;
+    }
+
+    /**
+     * Sets the properties associated with the tag.
+     *
+     * @param properties The properties associated with the tag.
+     * @return The builder instance.
+     */
+    public Builder withProperties(Map<String, String> properties) {
+      tagDTO.properties = properties;
+      return this;
+    }
+
+    /**
+     * Sets the audit information for the tag.
+     *
+     * @param audit The audit information for the tag.
+     * @return The builder instance.
+     */
+    public Builder withAudit(AuditDTO audit) {
+      tagDTO.audit = audit;
+      return this;
+    }
+
+    /**
+     * Sets whether the tag is inherited.
+     *
+     * @param inherited Whether the tag is inherited.
+     * @return The builder instance.
+     */
+    public Builder withInherited(Optional<Boolean> inherited) {
+      tagDTO.inherited = inherited;
+      return this;
+    }
+
+    /** @return The constructed Tag DTO. */
+    public TagDTO build() {
+      return tagDTO;
+    }
+  }
+}
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 5e253788a..fa6ae6b56 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
@@ -22,9 +22,11 @@ import static 
org.apache.gravitino.rel.expressions.transforms.Transforms.NAME_OF
 
 import java.util.Arrays;
 import java.util.Map;
+import java.util.Optional;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.gravitino.Audit;
 import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.Metalake;
 import org.apache.gravitino.Schema;
 import org.apache.gravitino.authorization.Group;
@@ -68,6 +70,8 @@ import 
org.apache.gravitino.dto.rel.partitions.IdentityPartitionDTO;
 import org.apache.gravitino.dto.rel.partitions.ListPartitionDTO;
 import org.apache.gravitino.dto.rel.partitions.PartitionDTO;
 import org.apache.gravitino.dto.rel.partitions.RangePartitionDTO;
+import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+import org.apache.gravitino.dto.tag.TagDTO;
 import org.apache.gravitino.file.Fileset;
 import org.apache.gravitino.messaging.Topic;
 import org.apache.gravitino.rel.Column;
@@ -92,6 +96,7 @@ import org.apache.gravitino.rel.partitions.Partition;
 import org.apache.gravitino.rel.partitions.Partitions;
 import org.apache.gravitino.rel.partitions.RangePartition;
 import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.Tag;
 
 /** Utility class for converting between DTOs and domain objects. */
 public class DTOConverters {
@@ -465,6 +470,39 @@ public class DTOConverters {
         .build();
   }
 
+  /**
+   * Converts a MetadataObject to a MetadataObjectDTO.
+   *
+   * @param metadataObject The metadata object to be converted.
+   * @return The metadata object DTO.
+   */
+  public static MetadataObjectDTO toDTO(MetadataObject metadataObject) {
+    return MetadataObjectDTO.builder()
+        .withParent(metadataObject.parent())
+        .withName(metadataObject.name())
+        .withType(metadataObject.type())
+        .build();
+  }
+
+  /**
+   * Converts a Tag to a TagDTO.
+   *
+   * @param tag The tag to be converted.
+   * @param inherited The inherited flag.
+   * @return The tag DTO.
+   */
+  public static TagDTO toDTO(Tag tag, Optional<Boolean> inherited) {
+    TagDTO.Builder builder =
+        TagDTO.builder()
+            .withName(tag.name())
+            .withComment(tag.comment())
+            .withProperties(tag.properties())
+            .withAudit(toDTO(tag.auditInfo()))
+            .withInherited(inherited);
+
+    return builder.build();
+  }
+
   /**
    * Converts a Expression to an FunctionArg DTO.
    *
diff --git a/common/src/main/java/org/apache/gravitino/json/JsonUtils.java 
b/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
index 76ba31163..b3f81cb67 100644
--- a/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
+++ b/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
@@ -34,6 +34,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.databind.cfg.EnumFeature;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -252,7 +253,8 @@ public class JsonUtils {
             .configure(EnumFeature.WRITE_ENUMS_TO_LOWERCASE, true)
             .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
             .build()
-            .registerModule(new JavaTimeModule());
+            .registerModule(new JavaTimeModule())
+            .registerModule(new Jdk8Module());
   }
 
   /**
@@ -288,7 +290,8 @@ public class JsonUtils {
             .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
             .build()
             .setVisibility(PropertyAccessor.FIELD, 
JsonAutoDetect.Visibility.ANY)
-            .registerModule(new JavaTimeModule());
+            .registerModule(new JavaTimeModule())
+            .registerModule(new Jdk8Module());
   }
 
   /**
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
new file mode 100644
index 000000000..5d90639ca
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagCreateRequest {
+
+  @Test
+  public void testTagCreateRequestSerDe() throws JsonProcessingException {
+    TagCreateRequest request = new TagCreateRequest("tag_test", "tag comment", 
null);
+    String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+    TagCreateRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, TagCreateRequest.class);
+    Assertions.assertEquals(request, deserRequest);
+    Assertions.assertEquals("tag_test", deserRequest.getName());
+    Assertions.assertEquals("tag comment", deserRequest.getComment());
+    Assertions.assertNull(deserRequest.getProperties());
+
+    Map<String, String> properties = ImmutableMap.of("key", "value");
+    TagCreateRequest request1 = new TagCreateRequest("tag_test", "tag 
comment", properties);
+    serJson = JsonUtils.objectMapper().writeValueAsString(request1);
+    TagCreateRequest deserRequest1 =
+        JsonUtils.objectMapper().readValue(serJson, TagCreateRequest.class);
+    Assertions.assertEquals(request1, deserRequest1);
+    Assertions.assertEquals(properties, deserRequest1.getProperties());
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagUpdatesRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagUpdatesRequest.java
new file mode 100644
index 000000000..8fb0a0bae
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagUpdatesRequest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.requests;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagUpdatesRequest {
+
+  @Test
+  public void testRenameTagRequestSerDe() throws JsonProcessingException {
+    TagUpdateRequest.RenameTagRequest request =
+        new TagUpdateRequest.RenameTagRequest("tag_test_new");
+    String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+    TagUpdateRequest.RenameTagRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, 
TagUpdateRequest.RenameTagRequest.class);
+    Assertions.assertEquals(request, deserRequest);
+    Assertions.assertEquals("tag_test_new", deserRequest.getNewName());
+  }
+
+  @Test
+  public void testUpdateTagCommentRequestSerDe() throws 
JsonProcessingException {
+    TagUpdateRequest.UpdateTagCommentRequest request =
+        new TagUpdateRequest.UpdateTagCommentRequest("tag comment new");
+    String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+    TagUpdateRequest.UpdateTagCommentRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, 
TagUpdateRequest.UpdateTagCommentRequest.class);
+    Assertions.assertEquals(request, deserRequest);
+    Assertions.assertEquals("tag comment new", deserRequest.getNewComment());
+  }
+
+  @Test
+  public void testSetTagPropertyRequestSerDe() throws JsonProcessingException {
+    TagUpdateRequest.SetTagPropertyRequest request =
+        new TagUpdateRequest.SetTagPropertyRequest("key", "value");
+    String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+    TagUpdateRequest.SetTagPropertyRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, 
TagUpdateRequest.SetTagPropertyRequest.class);
+    Assertions.assertEquals(request, deserRequest);
+    Assertions.assertEquals("key", deserRequest.getProperty());
+    Assertions.assertEquals("value", deserRequest.getValue());
+  }
+
+  @Test
+  public void testRemoveTagPropertyRequestSerDe() throws 
JsonProcessingException {
+    TagUpdateRequest.RemoveTagPropertyRequest request =
+        new TagUpdateRequest.RemoveTagPropertyRequest("key");
+    String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+    TagUpdateRequest.RemoveTagPropertyRequest deserRequest =
+        JsonUtils.objectMapper()
+            .readValue(serJson, 
TagUpdateRequest.RemoveTagPropertyRequest.class);
+    Assertions.assertEquals(request, deserRequest);
+    Assertions.assertEquals("key", deserRequest.getProperty());
+  }
+
+  @Test
+  public void testTagUpdatesRequestSerDe() throws JsonProcessingException {
+    TagUpdateRequest request = new 
TagUpdateRequest.RenameTagRequest("tag_test_new");
+    TagUpdateRequest request1 = new 
TagUpdateRequest.UpdateTagCommentRequest("tag comment new");
+    TagUpdateRequest request2 = new 
TagUpdateRequest.SetTagPropertyRequest("key", "value");
+    TagUpdateRequest request3 = new 
TagUpdateRequest.RemoveTagPropertyRequest("key");
+
+    List<TagUpdateRequest> updates = ImmutableList.of(request, request1, 
request2, request3);
+    TagUpdatesRequest tagUpdatesRequest = new TagUpdatesRequest(updates);
+    String serJson = 
JsonUtils.objectMapper().writeValueAsString(tagUpdatesRequest);
+    TagUpdatesRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, TagUpdatesRequest.class);
+    Assertions.assertEquals(tagUpdatesRequest, deserRequest);
+    Assertions.assertEquals(updates, deserRequest.getUpdates());
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java 
b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
index 7f9ebfeb7..242c0f953 100644
--- a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
+++ b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
@@ -18,11 +18,14 @@
  */
 package org.apache.gravitino.dto.responses;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.collect.Lists;
 import java.time.Instant;
 import org.apache.gravitino.Catalog;
@@ -41,7 +44,9 @@ import org.apache.gravitino.dto.authorization.UserDTO;
 import org.apache.gravitino.dto.rel.ColumnDTO;
 import org.apache.gravitino.dto.rel.TableDTO;
 import org.apache.gravitino.dto.rel.partitioning.Partitioning;
+import org.apache.gravitino.dto.tag.TagDTO;
 import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.json.JsonUtils;
 import org.apache.gravitino.rel.types.Types;
 import org.junit.jupiter.api.Test;
 
@@ -320,4 +325,56 @@ public class TestResponses {
     RoleResponse role = new RoleResponse();
     assertThrows(IllegalArgumentException.class, () -> role.validate());
   }
+
+  @Test
+  void testNameListResponse() throws JsonProcessingException {
+    String[] names = new String[] {"name1", "name2"};
+    NameListResponse response = new NameListResponse(names);
+    assertDoesNotThrow(response::validate);
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+    NameListResponse deserResponse =
+        JsonUtils.objectMapper().readValue(serJson, NameListResponse.class);
+    assertEquals(response, deserResponse);
+    assertArrayEquals(names, deserResponse.getNames());
+
+    NameListResponse response1 = new NameListResponse();
+    Exception e = assertThrows(IllegalArgumentException.class, 
response1::validate);
+    assertEquals("\"names\" must not be null", e.getMessage());
+  }
+
+  @Test
+  void testTagListResponse() throws JsonProcessingException {
+    TagDTO tag1 = 
TagDTO.builder().withName("tag1").withComment("comment1").build();
+    TagDTO tag2 = 
TagDTO.builder().withName("tag2").withComment("comment2").build();
+    TagDTO[] tags = new TagDTO[] {tag1, tag2};
+    TagListResponse response = new TagListResponse(tags);
+    assertDoesNotThrow(response::validate);
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+    TagListResponse deserResponse =
+        JsonUtils.objectMapper().readValue(serJson, TagListResponse.class);
+    assertEquals(response, deserResponse);
+    assertArrayEquals(tags, deserResponse.getTags());
+
+    TagListResponse response1 = new TagListResponse();
+    Exception e = assertThrows(IllegalArgumentException.class, 
response1::validate);
+    assertEquals("\"tags\" must not be null", e.getMessage());
+  }
+
+  @Test
+  void testTagResponse() throws JsonProcessingException {
+    TagDTO tag = 
TagDTO.builder().withName("tag1").withComment("comment1").build();
+    TagResponse response = new TagResponse(tag);
+    assertDoesNotThrow(response::validate);
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+    TagResponse deserResponse = JsonUtils.objectMapper().readValue(serJson, 
TagResponse.class);
+    assertEquals(response, deserResponse);
+    assertEquals(tag, deserResponse.getTag());
+
+    TagResponse response1 = new TagResponse();
+    Exception e = assertThrows(IllegalArgumentException.class, 
response1::validate);
+    assertEquals("\"tag\" must not be null", e.getMessage());
+  }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/tag/TestMetadataObjectDTO.java 
b/common/src/test/java/org/apache/gravitino/dto/tag/TestMetadataObjectDTO.java
new file mode 100644
index 000000000..573890cf1
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/tag/TestMetadataObjectDTO.java
@@ -0,0 +1,150 @@
+/*
+ * 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.tag;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestMetadataObjectDTO {
+
+  @Test
+  void testObjectSerDe() throws JsonProcessingException {
+
+    // Test metalake object
+    MetadataObjectDTO metalakeDTO =
+        MetadataObjectDTO.builder()
+            .withName("metalake_test")
+            .withType(MetadataObject.Type.METALAKE)
+            .build();
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(metalakeDTO);
+    String expected = "{\"fullName\":\"metalake_test\",\"type\":\"metalake\"}";
+    Assertions.assertEquals(
+        JsonUtils.objectMapper().readTree(expected), 
JsonUtils.objectMapper().readTree(serJson));
+
+    MetadataObjectDTO deserMetalakeDTO =
+        JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+    Assertions.assertEquals(metalakeDTO, deserMetalakeDTO);
+
+    // Test catalog object
+    MetadataObjectDTO catalogDTO =
+        MetadataObjectDTO.builder()
+            .withName("catalog_test")
+            .withType(MetadataObject.Type.CATALOG)
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(catalogDTO);
+    expected = "{\"fullName\":\"catalog_test\",\"type\":\"catalog\"}";
+    Assertions.assertEquals(
+        JsonUtils.objectMapper().readTree(expected), 
JsonUtils.objectMapper().readTree(serJson));
+
+    MetadataObjectDTO deserCatalogDTO =
+        JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+    Assertions.assertEquals(catalogDTO, deserCatalogDTO);
+
+    // Test schema object
+    MetadataObjectDTO schemaDTO =
+        MetadataObjectDTO.builder()
+            .withName("schema_test")
+            .withParent("catalog_test")
+            .withType(MetadataObject.Type.SCHEMA)
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(schemaDTO);
+    expected = 
"{\"fullName\":\"catalog_test.schema_test\",\"type\":\"schema\"}";
+    Assertions.assertEquals(
+        JsonUtils.objectMapper().readTree(expected), 
JsonUtils.objectMapper().readTree(serJson));
+
+    MetadataObjectDTO deserSchemaDTO =
+        JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+    Assertions.assertEquals(schemaDTO, deserSchemaDTO);
+
+    // Test table object
+    MetadataObjectDTO tableDTO =
+        MetadataObjectDTO.builder()
+            .withName("table_test")
+            .withParent("catalog_test.schema_test")
+            .withType(MetadataObject.Type.TABLE)
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(tableDTO);
+    expected = 
"{\"fullName\":\"catalog_test.schema_test.table_test\",\"type\":\"table\"}";
+    Assertions.assertEquals(
+        JsonUtils.objectMapper().readTree(expected), 
JsonUtils.objectMapper().readTree(serJson));
+
+    MetadataObjectDTO deserTableDTO =
+        JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+    Assertions.assertEquals(tableDTO, deserTableDTO);
+
+    // Test column object
+    MetadataObjectDTO columnDTO =
+        MetadataObjectDTO.builder()
+            .withName("column_test")
+            .withParent("catalog_test.schema_test.table_test")
+            .withType(MetadataObject.Type.COLUMN)
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(columnDTO);
+    expected =
+        
"{\"fullName\":\"catalog_test.schema_test.table_test.column_test\",\"type\":\"column\"}";
+    Assertions.assertEquals(
+        JsonUtils.objectMapper().readTree(expected), 
JsonUtils.objectMapper().readTree(serJson));
+
+    MetadataObjectDTO deserColumnDTO =
+        JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+    Assertions.assertEquals(columnDTO, deserColumnDTO);
+
+    // Test topic object
+    MetadataObjectDTO topicDTO =
+        MetadataObjectDTO.builder()
+            .withName("topic_test")
+            .withParent("catalog_test.schema_test")
+            .withType(MetadataObject.Type.TOPIC)
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(topicDTO);
+    expected = 
"{\"fullName\":\"catalog_test.schema_test.topic_test\",\"type\":\"topic\"}";
+    Assertions.assertEquals(
+        JsonUtils.objectMapper().readTree(expected), 
JsonUtils.objectMapper().readTree(serJson));
+
+    MetadataObjectDTO deserTopicDTO =
+        JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+    Assertions.assertEquals(topicDTO, deserTopicDTO);
+
+    // Test fileset object
+    MetadataObjectDTO filesetDTO =
+        MetadataObjectDTO.builder()
+            .withName("fileset_test")
+            .withParent("catalog_test.schema_test")
+            .withType(MetadataObject.Type.FILESET)
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(filesetDTO);
+    expected = 
"{\"fullName\":\"catalog_test.schema_test.fileset_test\",\"type\":\"fileset\"}";
+    Assertions.assertEquals(
+        JsonUtils.objectMapper().readTree(expected), 
JsonUtils.objectMapper().readTree(serJson));
+
+    MetadataObjectDTO deserFilesetDTO =
+        JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+    Assertions.assertEquals(filesetDTO, deserFilesetDTO);
+  }
+}
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
new file mode 100644
index 000000000..89acaa887
--- /dev/null
+++ b/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java
@@ -0,0 +1,103 @@
+/*
+ * 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.tag;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagDTO {
+
+  @Test
+  public void testTagSerDe() throws JsonProcessingException {
+    AuditDTO audit = 
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+    TagDTO tagDTO =
+        TagDTO.builder().withName("tag_test").withComment("tag 
comment").withAudit(audit).build();
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO);
+    TagDTO deserTagDTO = JsonUtils.objectMapper().readValue(serJson, 
TagDTO.class);
+    Assertions.assertEquals(tagDTO, deserTagDTO);
+
+    Assertions.assertEquals("tag_test", deserTagDTO.name());
+    Assertions.assertEquals("tag comment", deserTagDTO.comment());
+    Assertions.assertEquals(audit, deserTagDTO.auditInfo());
+    Assertions.assertNull(deserTagDTO.properties());
+
+    // Test tag with property
+    Map<String, String> properties = ImmutableMap.of("key", "value");
+    TagDTO tagDTO1 =
+        TagDTO.builder()
+            .withName("tag_test")
+            .withComment("tag comment")
+            .withAudit(audit)
+            .withProperties(properties)
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO1);
+    TagDTO deserTagDTO1 = JsonUtils.objectMapper().readValue(serJson, 
TagDTO.class);
+    Assertions.assertEquals(tagDTO1, deserTagDTO1);
+
+    Assertions.assertEquals(properties, deserTagDTO1.properties());
+
+    // Test tag with inherited
+    TagDTO tagDTO2 =
+        TagDTO.builder()
+            .withName("tag_test")
+            .withComment("tag comment")
+            .withAudit(audit)
+            .withInherited(Optional.empty())
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO2);
+    TagDTO deserTagDTO2 = JsonUtils.objectMapper().readValue(serJson, 
TagDTO.class);
+    Assertions.assertEquals(tagDTO2, deserTagDTO2);
+    Assertions.assertEquals(Optional.empty(), deserTagDTO2.inherited());
+
+    TagDTO tagDTO3 =
+        TagDTO.builder()
+            .withName("tag_test")
+            .withComment("tag comment")
+            .withAudit(audit)
+            .withInherited(Optional.of(false))
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO3);
+    TagDTO deserTagDTO3 = JsonUtils.objectMapper().readValue(serJson, 
TagDTO.class);
+    Assertions.assertEquals(Optional.of(false), deserTagDTO3.inherited());
+
+    TagDTO tagDTO4 =
+        TagDTO.builder()
+            .withName("tag_test")
+            .withComment("tag comment")
+            .withAudit(audit)
+            .withInherited(Optional.of(true))
+            .build();
+
+    serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO4);
+    TagDTO deserTagDTO4 = JsonUtils.objectMapper().readValue(serJson, 
TagDTO.class);
+    Assertions.assertEquals(Optional.of(true), deserTagDTO4.inherited());
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java 
b/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java
index fd635a024..eaea94e35 100644
--- a/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java
+++ b/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.gravitino.tag;
 
 import java.io.IOException;
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 1dc65e2d8..040ac9f19 100644
--- a/core/src/main/java/org/apache/gravitino/tag/TagManager.java
+++ b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
@@ -86,6 +86,10 @@ public class TagManager {
   }
 
   public String[] listTags(String metalake) {
+    return 
Arrays.stream(listTagsInfo(metalake)).map(Tag::name).toArray(String[]::new);
+  }
+
+  public Tag[] listTagsInfo(String metalake) {
     return TreeLockUtils.doWithTreeLock(
         NameIdentifier.of(ofTagNamespace(metalake).levels()),
         LockType.READ,
@@ -95,8 +99,7 @@ public class TagManager {
           try {
             return entityStore
                 .list(ofTagNamespace(metalake), TagEntity.class, 
Entity.EntityType.TAG).stream()
-                .map(TagEntity::name)
-                .toArray(String[]::new);
+                .toArray(Tag[]::new);
           } catch (IOException ioe) {
             LOG.error("Failed to list tags under metalake {}", metalake, ioe);
             throw new RuntimeException(ioe);
diff --git 
a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java 
b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
index 4d025e9ec..cf1bb7aac 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -44,6 +44,7 @@ import 
org.apache.gravitino.server.web.mapper.JsonMappingExceptionMapper;
 import org.apache.gravitino.server.web.mapper.JsonParseExceptionMapper;
 import org.apache.gravitino.server.web.mapper.JsonProcessingExceptionMapper;
 import org.apache.gravitino.server.web.ui.WebUIFilter;
+import org.apache.gravitino.tag.TagManager;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.jackson.JacksonFeature;
 import org.glassfish.jersey.server.ResourceConfig;
@@ -97,12 +98,12 @@ public class GravitinoServer extends ResourceConfig {
           protected void configure() {
             
bind(gravitinoEnv.metalakeDispatcher()).to(MetalakeDispatcher.class).ranked(1);
             
bind(gravitinoEnv.catalogDispatcher()).to(CatalogDispatcher.class).ranked(1);
-
             
bind(gravitinoEnv.schemaDispatcher()).to(SchemaDispatcher.class).ranked(1);
             
bind(gravitinoEnv.tableDispatcher()).to(TableDispatcher.class).ranked(1);
             
bind(gravitinoEnv.partitionDispatcher()).to(PartitionDispatcher.class).ranked(1);
             
bind(gravitinoEnv.filesetDispatcher()).to(FilesetDispatcher.class).ranked(1);
             
bind(gravitinoEnv.topicDispatcher()).to(TopicDispatcher.class).ranked(1);
+            bind(gravitinoEnv.tagManager()).to(TagManager.class).ranked(1);
           }
         });
     register(JsonProcessingExceptionMapper.class);
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
 
b/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
index 354c62319..fdf8ac911 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.cfg.EnumFeature;
 import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import javax.ws.rs.ext.ContextResolver;
 import javax.ws.rs.ext.Provider;
@@ -39,7 +40,8 @@ public class ObjectMapperProvider implements 
ContextResolver<ObjectMapper> {
             .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
             .build()
             .setSerializationInclusion(JsonInclude.Include.NON_NULL)
-            .registerModule(new JavaTimeModule());
+            .registerModule(new JavaTimeModule())
+            .registerModule(new Jdk8Module());
   }
 
   /**
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 677c42de9..459938ee9 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -35,6 +35,8 @@ import 
org.apache.gravitino.exceptions.PartitionAlreadyExistsException;
 import org.apache.gravitino.exceptions.RoleAlreadyExistsException;
 import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.apache.gravitino.exceptions.TableAlreadyExistsException;
+import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
+import org.apache.gravitino.exceptions.TagAlreadyExistsException;
 import org.apache.gravitino.exceptions.TopicAlreadyExistsException;
 import org.apache.gravitino.exceptions.UserAlreadyExistsException;
 import org.apache.gravitino.server.web.Utils;
@@ -107,6 +109,11 @@ public class ExceptionHandlers {
     return GroupPermissionOperationExceptionHandler.INSTANCE.handle(op, roles, 
parent, e);
   }
 
+  public static Response handleTagException(
+      OperationType op, String tag, String parent, Exception e) {
+    return TagExceptionHandler.INSTANCE.handle(op, tag, parent, e);
+  }
+
   public static Response handleTestConnectionException(Exception e) {
     ErrorResponse response;
     if (e instanceof IllegalArgumentException) {
@@ -519,6 +526,41 @@ public class ExceptionHandlers {
     }
   }
 
+  private static class TagExceptionHandler extends BaseExceptionHandler {
+
+    private static final ExceptionHandler INSTANCE = new TagExceptionHandler();
+
+    private static String getTagErrorMsg(
+        String tag, String operation, String parent, String reason) {
+      return String.format(
+          "Failed to operate tag(s)%s operation [%s] under object [%s], reason 
[%s]",
+          tag, operation, parent, reason);
+    }
+
+    @Override
+    public Response handle(OperationType op, String tag, String parent, 
Exception e) {
+      String formatted = StringUtil.isBlank(tag) ? "" : " [" + tag + "]";
+      String errorMsg = getTagErrorMsg(formatted, op.name(), parent, 
getErrorMsg(e));
+      LOG.warn(errorMsg, e);
+
+      if (e instanceof IllegalArgumentException) {
+        return Utils.illegalArguments(errorMsg, e);
+
+      } else if (e instanceof NotFoundException) {
+        return Utils.notFound(errorMsg, e);
+
+      } else if (e instanceof TagAlreadyExistsException) {
+        return Utils.alreadyExists(errorMsg, e);
+
+      } else if (e instanceof TagAlreadyAssociatedException) {
+        return Utils.alreadyExists(errorMsg, e);
+
+      } else {
+        return super.handle(op, tag, parent, e);
+      }
+    }
+  }
+
   @VisibleForTesting
   static class BaseExceptionHandler extends ExceptionHandler {
 
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
index 0adc4f831..9e611f6e2 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
@@ -30,5 +30,6 @@ public enum OperationType {
   REMOVE,
   DELETE,
   GRANT,
-  REVOKE
+  REVOKE,
+  ASSOCIATE,
 }
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
new file mode 100644
index 000000000..4a3abe7f9
--- /dev/null
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
@@ -0,0 +1,451 @@
+/*
+ * 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.server.web.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.TagCreateRequest;
+import org.apache.gravitino.dto.requests.TagUpdateRequest;
+import org.apache.gravitino.dto.requests.TagUpdatesRequest;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
+import org.apache.gravitino.dto.responses.NameListResponse;
+import org.apache.gravitino.dto.responses.TagListResponse;
+import org.apache.gravitino.dto.responses.TagResponse;
+import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+import org.apache.gravitino.dto.tag.TagDTO;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("metalakes/{metalake}/tags")
+public class TagOperations {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(TagOperations.class);
+
+  private final TagManager tagManager;
+
+  @Context private HttpServletRequest httpRequest;
+
+  @Inject
+  public TagOperations(TagManager tagManager) {
+    this.tagManager = tagManager;
+  }
+
+  @GET
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "list-tags." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "list-tags", absolute = true)
+  public Response listTags(
+      @PathParam("metalake") String metalake,
+      @QueryParam("details") @DefaultValue("false") boolean verbose) {
+    LOG.info(
+        "Received list tag {} request for metalake: {}", verbose ? "infos" : 
"names", metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            if (verbose) {
+              Tag[] tags = tagManager.listTagsInfo(metalake);
+              TagDTO[] tagDTOs;
+              if (ArrayUtils.isEmpty(tags)) {
+                tagDTOs = new TagDTO[0];
+              } else {
+                tagDTOs =
+                    Arrays.stream(tags)
+                        .map(t -> DTOConverters.toDTO(t, Optional.empty()))
+                        .toArray(TagDTO[]::new);
+              }
+
+              LOG.info("List {} tags info under metalake: {}", tagDTOs.length, 
metalake);
+              return Utils.ok(new TagListResponse(tagDTOs));
+
+            } else {
+              String[] tagNames = tagManager.listTags(metalake);
+              tagNames = tagNames == null ? new String[0] : tagNames;
+
+              LOG.info("List {} tags under metalake: {}", tagNames.length, 
metalake);
+              return Utils.ok(new NameListResponse(tagNames));
+            }
+          });
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.LIST, "", 
metalake, e);
+    }
+  }
+
+  @POST
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "create-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "create-tag", absolute = true)
+  public Response createTag(@PathParam("metalake") String metalake, 
TagCreateRequest request) {
+    LOG.info("Received create tag request under metalake: {}", metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            request.validate();
+            Tag tag =
+                tagManager.createTag(
+                    metalake, request.getName(), request.getComment(), 
request.getProperties());
+
+            LOG.info("Created tag: {} under metalake: {}", tag.name(), 
metalake);
+            return Utils.ok(new TagResponse(DTOConverters.toDTO(tag, 
Optional.empty())));
+          });
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(
+          OperationType.CREATE, request.getName(), metalake, e);
+    }
+  }
+
+  @GET
+  @Path("{tag}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "get-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "get-tag", absolute = true)
+  public Response getTag(@PathParam("metalake") String metalake, 
@PathParam("tag") String name) {
+    LOG.info("Received get tag request for tag: {} under metalake: {}", name, 
metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            Tag tag = tagManager.getTag(metalake, name);
+            LOG.info("Get tag: {} under metalake: {}", name, metalake);
+            return Utils.ok(new TagResponse(DTOConverters.toDTO(tag, 
Optional.empty())));
+          });
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.GET, name, 
metalake, e);
+    }
+  }
+
+  @PUT
+  @Path("{tag}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "alter-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "alter-tag", absolute = true)
+  public Response alterTag(
+      @PathParam("metalake") String metalake,
+      @PathParam("tag") String name,
+      TagUpdatesRequest request) {
+    LOG.info("Received alter tag request for tag: {} under metalake: {}", 
name, metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            request.validate();
+
+            TagChange[] changes =
+                request.getUpdates().stream()
+                    .map(TagUpdateRequest::tagChange)
+                    .toArray(TagChange[]::new);
+            Tag tag = tagManager.alterTag(metalake, name, changes);
+
+            LOG.info("Altered tag: {} under metalake: {}", name, metalake);
+            return Utils.ok(new TagResponse(DTOConverters.toDTO(tag, 
Optional.empty())));
+          });
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.ALTER, name, 
metalake, e);
+    }
+  }
+
+  @DELETE
+  @Path("{tag}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "delete-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "delete-tag", absolute = true)
+  public Response deleteTag(@PathParam("metalake") String metalake, 
@PathParam("tag") String name) {
+    LOG.info("Received delete tag request for tag: {} under metalake: {}", 
name, metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            boolean deleted = tagManager.deleteTag(metalake, name);
+            if (!deleted) {
+              LOG.warn("Failed to delete tag {} under metalake {}", name, 
metalake);
+            } else {
+              LOG.info("Deleted tag: {} under metalake: {}", name, metalake);
+            }
+
+            return Utils.ok(new DropResponse(deleted));
+          });
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.DELETE, name, 
metalake, e);
+    }
+  }
+
+  @GET
+  @Path("{type}/{fullName}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "list-object-tags." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "list-object-tags", absolute = true)
+  public Response listTagsForMetadataObject(
+      @PathParam("metalake") String metalake,
+      @PathParam("type") String type,
+      @PathParam("fullName") String fullName,
+      @QueryParam("details") @DefaultValue("false") boolean verbose) {
+    LOG.info(
+        "Received list tag {} request for object type: {}, full name: {} under 
metalake: {}",
+        verbose ? "infos" : "names",
+        type,
+        fullName,
+        metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            MetadataObject object =
+                MetadataObjects.parse(
+                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+
+            List<TagDTO> tags = Lists.newArrayList();
+            Tag[] nonInheritedTags = 
tagManager.listTagsInfoForMetadataObject(metalake, object);
+            if (ArrayUtils.isNotEmpty(nonInheritedTags)) {
+              Collections.addAll(
+                  tags,
+                  Arrays.stream(nonInheritedTags)
+                      .map(t -> DTOConverters.toDTO(t, Optional.of(false)))
+                      .toArray(TagDTO[]::new));
+            }
+
+            MetadataObject parentObject = MetadataObjects.parent(object);
+            while (parentObject != null) {
+              Tag[] inheritedTags =
+                  tagManager.listTagsInfoForMetadataObject(metalake, 
parentObject);
+              if (ArrayUtils.isNotEmpty(inheritedTags)) {
+                Collections.addAll(
+                    tags,
+                    Arrays.stream(inheritedTags)
+                        .map(t -> DTOConverters.toDTO(t, Optional.of(true)))
+                        .toArray(TagDTO[]::new));
+              }
+              parentObject = MetadataObjects.parent(parentObject);
+            }
+
+            if (verbose) {
+              LOG.info(
+                  "List {} tags info for object type: {}, full name: {} under 
metalake: {}",
+                  tags.size(),
+                  type,
+                  fullName,
+                  metalake);
+              return Utils.ok(new TagListResponse(tags.toArray(new 
TagDTO[0])));
+
+            } else {
+              String[] tagNames = 
tags.stream().map(TagDTO::name).toArray(String[]::new);
+
+              LOG.info(
+                  "List {} tags for object type: {}, full name: {} under 
metalake: {}",
+                  tagNames.length,
+                  type,
+                  fullName,
+                  metalake);
+              return Utils.ok(new NameListResponse(tagNames));
+            }
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.LIST, "", 
fullName, e);
+    }
+  }
+
+  @GET
+  @Path("{type}/{fullName}/{tag}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "get-object-tag." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "get-object-tag", absolute = true)
+  public Response getTagForObject(
+      @PathParam("metalake") String metalake,
+      @PathParam("type") String type,
+      @PathParam("fullName") String fullName,
+      @PathParam("tag") String tagName) {
+    LOG.info(
+        "Received get tag {} request for object type: {}, full name: {} under 
metalake: {}",
+        tagName,
+        type,
+        fullName,
+        metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            MetadataObject object =
+                MetadataObjects.parse(
+                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+            Optional<Tag> tag = getTagForObject(metalake, object, tagName);
+            Optional<TagDTO> tagDTO = tag.map(t -> DTOConverters.toDTO(t, 
Optional.of(false)));
+
+            MetadataObject parentObject = MetadataObjects.parent(object);
+            while (!tag.isPresent() && parentObject != null) {
+              tag = getTagForObject(metalake, parentObject, tagName);
+              tagDTO = tag.map(t -> DTOConverters.toDTO(t, Optional.of(true)));
+              parentObject = MetadataObjects.parent(parentObject);
+            }
+
+            if (!tagDTO.isPresent()) {
+              LOG.warn(
+                  "Tag {} not found for object type: {}, full name: {} under 
metalake: {}",
+                  tagName,
+                  type,
+                  fullName,
+                  metalake);
+              return Utils.notFound(
+                  NoSuchTagException.class.getSimpleName(),
+                  "Tag not found: "
+                      + tagName
+                      + " for object type: "
+                      + type
+                      + ", full name: "
+                      + fullName
+                      + " under metalake: "
+                      + metalake);
+            } else {
+              LOG.info(
+                  "Get tag: {} for object type: {}, full name: {} under 
metalake: {}",
+                  tagName,
+                  type,
+                  fullName,
+                  metalake);
+              return Utils.ok(new TagResponse(tagDTO.get()));
+            }
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.GET, tagName, 
fullName, e);
+    }
+  }
+
+  @GET
+  @Path("{tag}/objects")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "list-objects-for-tag." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "list-objects-for-tag", absolute = true)
+  public Response listMetadataObjectsForTag(
+      @PathParam("metalake") String metalake, @PathParam("tag") String 
tagName) {
+    LOG.info("Received list objects for tag: {} under metalake: {}", tagName, 
metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            MetadataObject[] objects = 
tagManager.listMetadataObjectsForTag(metalake, tagName);
+            objects = objects == null ? new MetadataObject[0] : objects;
+
+            LOG.info(
+                "List {} objects for tag: {} under metalake: {}",
+                objects.length,
+                tagName,
+                metalake);
+
+            MetadataObjectDTO[] objectDTOs =
+                
Arrays.stream(objects).map(DTOConverters::toDTO).toArray(MetadataObjectDTO[]::new);
+            return Utils.ok(new MetadataObjectListResponse(objectDTOs));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.LIST, "", 
tagName, e);
+    }
+  }
+
+  @POST
+  @Path("{type}/{fullName}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "associate-object-tags." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "associate-object-tags", absolute = true)
+  public Response associateTagsForObject(
+      @PathParam("metalake") String metalake,
+      @PathParam("type") String type,
+      @PathParam("fullName") String fullName,
+      TagsAssociateRequest request) {
+    LOG.info(
+        "Received associate tags request for object type: {}, full name: {} 
under metalake: {}",
+        type,
+        fullName,
+        metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            request.validate();
+            MetadataObject object =
+                MetadataObjects.parse(
+                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+            String[] tagNames =
+                tagManager.associateTagsForMetadataObject(
+                    metalake, object, request.getTagsToAdd(), 
request.getTagsToRemove());
+            tagNames = tagNames == null ? new String[0] : tagNames;
+
+            LOG.info(
+                "Associated tags: {} for object type: {}, full name: {} under 
metalake: {}",
+                Arrays.toString(tagNames),
+                type,
+                fullName,
+                metalake);
+            return Utils.ok(new NameListResponse(tagNames));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleTagException(OperationType.ASSOCIATE, "", 
fullName, e);
+    }
+  }
+
+  private Optional<Tag> getTagForObject(String metalake, MetadataObject 
object, String tagName) {
+    try {
+      return Optional.ofNullable(tagManager.getTagForMetadataObject(metalake, 
object, tagName));
+    } catch (NoSuchTagException e) {
+      LOG.info("Tag {} not found for object: {}", tagName, object);
+      return Optional.empty();
+    }
+  }
+}
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
new file mode 100644
index 000000000..23b87b60d
--- /dev/null
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
@@ -0,0 +1,1099 @@
+/*
+ * 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.server.web.rest;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.TagCreateRequest;
+import org.apache.gravitino.dto.requests.TagUpdateRequest;
+import org.apache.gravitino.dto.requests.TagUpdatesRequest;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
+import org.apache.gravitino.dto.responses.NameListResponse;
+import org.apache.gravitino.dto.responses.TagListResponse;
+import org.apache.gravitino.dto.responses.TagResponse;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
+import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagManager;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagOperations extends JerseyTest {
+
+  private static class MockServletRequestFactory extends 
ServletRequestFactoryBase {
+
+    @Override
+    public HttpServletRequest get() {
+      HttpServletRequest request = mock(HttpServletRequest.class);
+      when(request.getRemoteUser()).thenReturn(null);
+      return request;
+    }
+  }
+
+  private TagManager tagManager = mock(TagManager.class);
+
+  private String metalake = "test_metalake";
+
+  private AuditInfo testAuditInfo1 =
+      
AuditInfo.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+  @Override
+  protected Application configure() {
+    try {
+      forceSet(
+          TestProperties.CONTAINER_PORT, 
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    ResourceConfig resourceConfig = new ResourceConfig();
+    resourceConfig.register(TagOperations.class);
+    resourceConfig.register(
+        new AbstractBinder() {
+          @Override
+          protected void configure() {
+            bind(tagManager).to(TagManager.class).ranked(2);
+            bindFactory(TestTagOperations.MockServletRequestFactory.class)
+                .to(HttpServletRequest.class);
+          }
+        });
+
+    return resourceConfig;
+  }
+
+  @Test
+  public void testListTags() {
+    String[] tags = new String[] {"tag1", "tag2"};
+    when(tagManager.listTags(metalake)).thenReturn(tags);
+
+    Response response =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
response.getMediaType());
+
+    NameListResponse nameListResponse = 
response.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse.getCode());
+    Assertions.assertArrayEquals(tags, nameListResponse.getNames());
+
+    when(tagManager.listTags(metalake)).thenReturn(null);
+    Response resp1 =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp1.getStatus());
+
+    NameListResponse nameListResponse1 = 
resp1.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse1.getCode());
+    Assertions.assertEquals(0, nameListResponse1.getNames().length);
+
+    when(tagManager.listTags(metalake)).thenReturn(new String[0]);
+    Response resp2 =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp2.getStatus());
+
+    NameListResponse nameListResponse2 = 
resp2.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse2.getCode());
+    Assertions.assertEquals(0, nameListResponse2.getNames().length);
+
+    // Test throw NoSuchMetalakeException
+    doThrow(new NoSuchMetalakeException("mock 
error")).when(tagManager).listTags(metalake);
+    Response resp3 =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp3.getStatus());
+
+    ErrorResponse errorResp = resp3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResp.getCode());
+    Assertions.assertEquals(NoSuchMetalakeException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(tagManager).listTags(metalake);
+    Response resp4 =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp4.getStatus());
+
+    ErrorResponse errorResp1 = resp4.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp1.getType());
+  }
+
+  @Test
+  public void testListTagsInfo() {
+    TagEntity tag1 =
+        TagEntity.builder()
+            .withName("tag1")
+            .withId(1L)
+            .withComment("tag1 comment")
+            .withAuditInfo(testAuditInfo1)
+            .build();
+
+    TagEntity tag2 =
+        TagEntity.builder()
+            .withName("tag2")
+            .withId(1L)
+            .withComment("tag2 comment")
+            .withAuditInfo(testAuditInfo1)
+            .build();
+
+    Tag[] tags = new Tag[] {tag1, tag2};
+    when(tagManager.listTagsInfo(metalake)).thenReturn(tags);
+
+    Response resp =
+        target(tagPath(metalake))
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    TagListResponse tagListResp = resp.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResp.getCode());
+    Assertions.assertEquals(tags.length, tagListResp.getTags().length);
+
+    Assertions.assertEquals(tag1.name(), tagListResp.getTags()[0].name());
+    Assertions.assertEquals(tag1.comment(), 
tagListResp.getTags()[0].comment());
+    Assertions.assertEquals(Optional.empty(), 
tagListResp.getTags()[0].inherited());
+
+    Assertions.assertEquals(tag2.name(), tagListResp.getTags()[1].name());
+    Assertions.assertEquals(tag2.comment(), 
tagListResp.getTags()[1].comment());
+    Assertions.assertEquals(Optional.empty(), 
tagListResp.getTags()[1].inherited());
+
+    // Test return null
+    when(tagManager.listTagsInfo(metalake)).thenReturn(null);
+    Response resp1 =
+        target(tagPath(metalake))
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp1.getStatus());
+
+    TagListResponse tagListResp1 = resp1.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResp1.getCode());
+    Assertions.assertEquals(0, tagListResp1.getTags().length);
+
+    // Test return empty array
+    when(tagManager.listTagsInfo(metalake)).thenReturn(new Tag[0]);
+    Response resp2 =
+        target(tagPath(metalake))
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp2.getStatus());
+
+    TagListResponse tagListResp2 = resp2.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResp2.getCode());
+    Assertions.assertEquals(0, tagListResp2.getTags().length);
+  }
+
+  @Test
+  public void testCreateTag() {
+    TagEntity tag1 =
+        TagEntity.builder()
+            .withName("tag1")
+            .withId(1L)
+            .withComment("tag1 comment")
+            .withAuditInfo(testAuditInfo1)
+            .build();
+    when(tagManager.createTag(metalake, "tag1", "tag1 comment", 
null)).thenReturn(tag1);
+
+    TagCreateRequest request = new TagCreateRequest("tag1", "tag1 comment", 
null);
+    Response resp =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    TagResponse tagResp = resp.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResp.getCode());
+
+    Tag respTag = tagResp.getTag();
+    Assertions.assertEquals(tag1.name(), respTag.name());
+    Assertions.assertEquals(tag1.comment(), respTag.comment());
+    Assertions.assertEquals(Optional.empty(), respTag.inherited());
+
+    // Test throw TagAlreadyExistsException
+    doThrow(new TagAlreadyExistsException("mock error"))
+        .when(tagManager)
+        .createTag(any(), any(), any(), any());
+    Response resp1 =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp1.getStatus());
+
+    ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE, 
errorResp.getCode());
+    Assertions.assertEquals(TagAlreadyExistsException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(tagManager)
+        .createTag(any(), any(), any(), any());
+
+    Response resp2 =
+        target(tagPath(metalake))
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp2.getStatus());
+
+    ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp1.getType());
+  }
+
+  @Test
+  public void testGetTag() {
+    TagEntity tag1 =
+        TagEntity.builder()
+            .withName("tag1")
+            .withId(1L)
+            .withComment("tag1 comment")
+            .withAuditInfo(testAuditInfo1)
+            .build();
+    when(tagManager.getTag(metalake, "tag1")).thenReturn(tag1);
+
+    Response resp =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    TagResponse tagResp = resp.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResp.getCode());
+
+    Tag respTag = tagResp.getTag();
+    Assertions.assertEquals(tag1.name(), respTag.name());
+    Assertions.assertEquals(tag1.comment(), respTag.comment());
+    Assertions.assertEquals(Optional.empty(), respTag.inherited());
+
+    // Test throw NoSuchTagException
+    doThrow(new NoSuchTagException("mock 
error")).when(tagManager).getTag(metalake, "tag1");
+
+    Response resp2 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp2.getStatus());
+
+    ErrorResponse errorResp = resp2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResp.getCode());
+    Assertions.assertEquals(NoSuchTagException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(tagManager).getTag(metalake, "tag1");
+
+    Response resp3 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp3.getStatus());
+
+    ErrorResponse errorResp1 = resp3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp1.getType());
+  }
+
+  @Test
+  public void testAlterTag() {
+    TagEntity newTag =
+        TagEntity.builder()
+            .withName("new_tag1")
+            .withId(1L)
+            .withComment("new tag1 comment")
+            .withAuditInfo(testAuditInfo1)
+            .build();
+
+    TagChange[] changes =
+        new TagChange[] {TagChange.rename("new_tag1"), 
TagChange.updateComment("new tag1 comment")};
+
+    when(tagManager.alterTag(metalake, "tag1", changes)).thenReturn(newTag);
+
+    TagUpdateRequest[] requests =
+        new TagUpdateRequest[] {
+          new TagUpdateRequest.RenameTagRequest("new_tag1"),
+          new TagUpdateRequest.UpdateTagCommentRequest("new tag1 comment")
+        };
+    TagUpdatesRequest request = new 
TagUpdatesRequest(Lists.newArrayList(requests));
+    Response resp =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    TagResponse tagResp = resp.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResp.getCode());
+
+    Tag respTag = tagResp.getTag();
+    Assertions.assertEquals(newTag.name(), respTag.name());
+    Assertions.assertEquals(newTag.comment(), respTag.comment());
+    Assertions.assertEquals(Optional.empty(), respTag.inherited());
+
+    // Test throw NoSuchTagException
+    doThrow(new NoSuchTagException("mock 
error")).when(tagManager).alterTag(any(), any(), any());
+
+    Response resp1 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp1.getStatus());
+
+    ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResp.getCode());
+    Assertions.assertEquals(NoSuchTagException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(tagManager).alterTag(any(), any(), any());
+
+    Response resp2 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp2.getStatus());
+
+    ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp1.getType());
+  }
+
+  @Test
+  public void testDeleteTag() {
+    when(tagManager.deleteTag(metalake, "tag1")).thenReturn(true);
+
+    Response resp =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    DropResponse dropResp = resp.readEntity(DropResponse.class);
+    Assertions.assertEquals(0, dropResp.getCode());
+    Assertions.assertTrue(dropResp.dropped());
+
+    when(tagManager.deleteTag(metalake, "tag1")).thenReturn(false);
+    Response resp1 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp1.getStatus());
+
+    DropResponse dropResp1 = resp1.readEntity(DropResponse.class);
+    Assertions.assertEquals(0, dropResp1.getCode());
+    Assertions.assertFalse(dropResp1.dropped());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(tagManager).deleteTag(any(), any());
+
+    Response resp2 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp2.getStatus());
+
+    ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp1.getType());
+  }
+
+  @Test
+  public void testListTagsForObject() {
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+    MetadataObject schema = MetadataObjects.parse("object1.object2", 
MetadataObject.Type.SCHEMA);
+    MetadataObject table =
+        MetadataObjects.parse("object1.object2.object3", 
MetadataObject.Type.TABLE);
+    MetadataObject column =
+        MetadataObjects.parse("object1.object2.object3.object4", 
MetadataObject.Type.COLUMN);
+
+    Tag[] catalogTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag1").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
catalog)).thenReturn(catalogTagInfos);
+
+    Tag[] schemaTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag3").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
schema)).thenReturn(schemaTagInfos);
+
+    Tag[] tableTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag5").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
table)).thenReturn(tableTagInfos);
+
+    Tag[] columnTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag7").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
column)).thenReturn(columnTagInfos);
+
+    // Test catalog tags
+    Response response =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+
+    TagListResponse tagListResponse = 
response.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse.getCode());
+    Assertions.assertEquals(catalogTagInfos.length, 
tagListResponse.getTags().length);
+
+    Map<String, Tag> resultTags =
+        Arrays.stream(tagListResponse.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags.containsKey("tag1"));
+    Assertions.assertFalse(resultTags.get("tag1").inherited().get());
+
+    Response response1 =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response1.getStatus());
+
+    NameListResponse nameListResponse = 
response1.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse.getCode());
+    Assertions.assertEquals(catalogTagInfos.length, 
nameListResponse.getNames().length);
+    Assertions.assertArrayEquals(
+        Arrays.stream(catalogTagInfos).map(Tag::name).toArray(String[]::new),
+        nameListResponse.getNames());
+
+    // Test schema tags
+    Response response2 =
+        target(tagPath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response2.getStatus());
+
+    TagListResponse tagListResponse1 = 
response2.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse1.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length, 
tagListResponse1.getTags().length);
+
+    Map<String, Tag> resultTags1 =
+        Arrays.stream(tagListResponse1.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags1.containsKey("tag1"));
+    Assertions.assertTrue(resultTags1.containsKey("tag3"));
+
+    Assertions.assertTrue(resultTags1.get("tag1").inherited().get());
+    Assertions.assertFalse(resultTags1.get("tag3").inherited().get());
+
+    Response response3 =
+        target(tagPath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response3.getStatus());
+
+    NameListResponse nameListResponse1 = 
response3.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse1.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length, 
nameListResponse1.getNames().length);
+    Set<String> resultNames = Sets.newHashSet(nameListResponse1.getNames());
+    Assertions.assertTrue(resultNames.contains("tag1"));
+    Assertions.assertTrue(resultNames.contains("tag3"));
+
+    // Test table tags
+    Response response4 =
+        target(tagPath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response4.getStatus());
+
+    TagListResponse tagListResponse2 = 
response4.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse2.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length + tableTagInfos.length,
+        tagListResponse2.getTags().length);
+
+    Map<String, Tag> resultTags2 =
+        Arrays.stream(tagListResponse2.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags2.containsKey("tag1"));
+    Assertions.assertTrue(resultTags2.containsKey("tag3"));
+    Assertions.assertTrue(resultTags2.containsKey("tag5"));
+
+    Assertions.assertTrue(resultTags2.get("tag1").inherited().get());
+    Assertions.assertTrue(resultTags2.get("tag3").inherited().get());
+    Assertions.assertFalse(resultTags2.get("tag5").inherited().get());
+
+    Response response5 =
+        target(tagPath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response5.getStatus());
+
+    NameListResponse nameListResponse2 = 
response5.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse2.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length + tableTagInfos.length,
+        nameListResponse2.getNames().length);
+
+    Set<String> resultNames1 = Sets.newHashSet(nameListResponse2.getNames());
+    Assertions.assertTrue(resultNames1.contains("tag1"));
+    Assertions.assertTrue(resultNames1.contains("tag3"));
+    Assertions.assertTrue(resultNames1.contains("tag5"));
+
+    // Test column tags
+    Response response6 =
+        target(tagPath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response6.getStatus());
+
+    TagListResponse tagListResponse3 = 
response6.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse3.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length
+            + catalogTagInfos.length
+            + tableTagInfos.length
+            + columnTagInfos.length,
+        tagListResponse3.getTags().length);
+
+    Map<String, Tag> resultTags3 =
+        Arrays.stream(tagListResponse3.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags3.containsKey("tag1"));
+    Assertions.assertTrue(resultTags3.containsKey("tag3"));
+    Assertions.assertTrue(resultTags3.containsKey("tag5"));
+    Assertions.assertTrue(resultTags3.containsKey("tag7"));
+
+    Assertions.assertTrue(resultTags3.get("tag1").inherited().get());
+    Assertions.assertTrue(resultTags3.get("tag3").inherited().get());
+    Assertions.assertTrue(resultTags3.get("tag5").inherited().get());
+    Assertions.assertFalse(resultTags3.get("tag7").inherited().get());
+
+    Response response7 =
+        target(tagPath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response7.getStatus());
+
+    NameListResponse nameListResponse3 = 
response7.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse3.getCode());
+
+    Assertions.assertEquals(
+        schemaTagInfos.length
+            + catalogTagInfos.length
+            + tableTagInfos.length
+            + columnTagInfos.length,
+        nameListResponse3.getNames().length);
+
+    Set<String> resultNames2 = Sets.newHashSet(nameListResponse3.getNames());
+    Assertions.assertTrue(resultNames2.contains("tag1"));
+    Assertions.assertTrue(resultNames2.contains("tag3"));
+    Assertions.assertTrue(resultNames2.contains("tag5"));
+    Assertions.assertTrue(resultNames2.contains("tag7"));
+  }
+
+  @Test
+  public void testGetTagForObject() {
+    TagEntity tag1 =
+        
TagEntity.builder().withName("tag1").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+    when(tagManager.getTagForMetadataObject(metalake, catalog, 
"tag1")).thenReturn(tag1);
+
+    TagEntity tag2 =
+        
TagEntity.builder().withName("tag2").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject schema = MetadataObjects.parse("object1.object2", 
MetadataObject.Type.SCHEMA);
+    when(tagManager.getTagForMetadataObject(metalake, schema, 
"tag2")).thenReturn(tag2);
+
+    TagEntity tag3 =
+        
TagEntity.builder().withName("tag3").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject table =
+        MetadataObjects.parse("object1.object2.object3", 
MetadataObject.Type.TABLE);
+    when(tagManager.getTagForMetadataObject(metalake, table, 
"tag3")).thenReturn(tag3);
+
+    TagEntity tag4 =
+        
TagEntity.builder().withName("tag4").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject column =
+        MetadataObjects.parse("object1.object2.object3.object4", 
MetadataObject.Type.COLUMN);
+    when(tagManager.getTagForMetadataObject(metalake, column, 
"tag4")).thenReturn(tag4);
+
+    // Test catalog tag
+    Response response =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+
+    TagResponse tagResponse = response.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse.getCode());
+
+    Tag respTag = tagResponse.getTag();
+    Assertions.assertEquals(tag1.name(), respTag.name());
+    Assertions.assertEquals(tag1.comment(), respTag.comment());
+    Assertions.assertFalse(respTag.inherited().get());
+
+    // Test schema tag
+    Response response1 =
+        target(tagPath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tag2")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response1.getStatus());
+
+    TagResponse tagResponse1 = response1.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse1.getCode());
+
+    Tag respTag1 = tagResponse1.getTag();
+    Assertions.assertEquals(tag2.name(), respTag1.name());
+    Assertions.assertEquals(tag2.comment(), respTag1.comment());
+    Assertions.assertFalse(respTag1.inherited().get());
+
+    // Test table tag
+    Response response2 =
+        target(tagPath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tag3")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response2.getStatus());
+
+    TagResponse tagResponse2 = response2.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse2.getCode());
+
+    Tag respTag2 = tagResponse2.getTag();
+    Assertions.assertEquals(tag3.name(), respTag2.name());
+    Assertions.assertEquals(tag3.comment(), respTag2.comment());
+    Assertions.assertFalse(respTag2.inherited().get());
+
+    // Test column tag
+    Response response3 =
+        target(tagPath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .path("tag4")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response3.getStatus());
+
+    TagResponse tagResponse3 = response3.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse3.getCode());
+
+    Tag respTag3 = tagResponse3.getTag();
+    Assertions.assertEquals(tag4.name(), respTag3.name());
+    Assertions.assertEquals(tag4.comment(), respTag3.comment());
+    Assertions.assertFalse(respTag3.inherited().get());
+
+    // Test get schema inherited tag
+    Response response4 =
+        target(tagPath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response4.getStatus());
+
+    TagResponse tagResponse4 = response4.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse4.getCode());
+
+    Tag respTag4 = tagResponse4.getTag();
+    Assertions.assertEquals(tag1.name(), respTag4.name());
+    Assertions.assertEquals(tag1.comment(), respTag4.comment());
+    Assertions.assertTrue(respTag4.inherited().get());
+
+    // Test get table inherited tag
+    Response response5 =
+        target(tagPath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tag2")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response5.getStatus());
+
+    TagResponse tagResponse5 = response5.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse5.getCode());
+
+    Tag respTag5 = tagResponse5.getTag();
+    Assertions.assertEquals(tag2.name(), respTag5.name());
+    Assertions.assertEquals(tag2.comment(), respTag5.comment());
+    Assertions.assertTrue(respTag5.inherited().get());
+
+    // Test get column inherited tag
+    Response response6 =
+        target(tagPath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .path("tag3")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response6.getStatus());
+
+    TagResponse tagResponse6 = response6.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse6.getCode());
+
+    Tag respTag6 = tagResponse6.getTag();
+    Assertions.assertEquals(tag3.name(), respTag6.name());
+    Assertions.assertEquals(tag3.comment(), respTag6.comment());
+    Assertions.assertTrue(respTag6.inherited().get());
+
+    // Test catalog tag throw NoSuchTagException
+    Response response7 =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tag2")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response7.getStatus());
+
+    ErrorResponse errorResponse = response7.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResponse.getCode());
+    Assertions.assertEquals(NoSuchTagException.class.getSimpleName(), 
errorResponse.getType());
+
+    // Test schema tag throw NoSuchTagException
+    Response response8 =
+        target(tagPath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tag3")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response8.getStatus());
+
+    ErrorResponse errorResponse1 = response8.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResponse1.getCode());
+    Assertions.assertEquals(NoSuchTagException.class.getSimpleName(), 
errorResponse1.getType());
+  }
+
+  @Test
+  public void testAssociateTagsForObject() {
+    String[] tagsToAdd = new String[] {"tag1", "tag2"};
+    String[] tagsToRemove = new String[] {"tag3", "tag4"};
+
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+    when(tagManager.associateTagsForMetadataObject(metalake, catalog, 
tagsToAdd, tagsToRemove))
+        .thenReturn(tagsToAdd);
+
+    TagsAssociateRequest request = new TagsAssociateRequest(tagsToAdd, 
tagsToRemove);
+    Response response =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
response.getMediaType());
+
+    NameListResponse nameListResponse = 
response.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse.getCode());
+
+    Assertions.assertArrayEquals(tagsToAdd, nameListResponse.getNames());
+
+    // Test throw null tags
+    when(tagManager.associateTagsForMetadataObject(metalake, catalog, 
tagsToAdd, tagsToRemove))
+        .thenReturn(null);
+    Response response1 =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response1.getStatus());
+
+    NameListResponse nameListResponse1 = 
response1.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse1.getCode());
+
+    Assertions.assertEquals(0, nameListResponse1.getNames().length);
+
+    // Test throw TagAlreadyAssociatedException
+    doThrow(new TagAlreadyAssociatedException("mock error"))
+        .when(tagManager)
+        .associateTagsForMetadataObject(metalake, catalog, tagsToAdd, 
tagsToRemove);
+    Response response2 =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
response2.getStatus());
+
+    ErrorResponse errorResponse = response2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE, 
errorResponse.getCode());
+    Assertions.assertEquals(
+        TagAlreadyAssociatedException.class.getSimpleName(), 
errorResponse.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(tagManager)
+        .associateTagsForMetadataObject(any(), any(), any(), any());
+
+    Response response3 =
+        target(tagPath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response3.getStatus());
+
+    ErrorResponse errorResponse1 = response3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResponse1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse1.getType());
+  }
+
+  @Test
+  public void testListMetadataObjectForTag() {
+    MetadataObject[] objects =
+        new MetadataObject[] {
+          MetadataObjects.parse("object1", MetadataObject.Type.CATALOG),
+          MetadataObjects.parse("object1.object2", MetadataObject.Type.SCHEMA),
+          MetadataObjects.parse("object1.object2.object3", 
MetadataObject.Type.TABLE),
+          MetadataObjects.parse("object1.object2.object3.object4", 
MetadataObject.Type.COLUMN)
+        };
+
+    when(tagManager.listMetadataObjectsForTag(metalake, 
"tag1")).thenReturn(objects);
+
+    Response response =
+        target(tagPath(metalake))
+            .path("tag1")
+            .path("objects")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
response.getMediaType());
+
+    MetadataObjectListResponse objectListResponse =
+        response.readEntity(MetadataObjectListResponse.class);
+    Assertions.assertEquals(0, objectListResponse.getCode());
+
+    MetadataObject[] respObjects = objectListResponse.getMetadataObjects();
+    Assertions.assertEquals(objects.length, respObjects.length);
+
+    for (int i = 0; i < objects.length; i++) {
+      Assertions.assertEquals(objects[i].type(), respObjects[i].type());
+      Assertions.assertEquals(objects[i].fullName(), 
respObjects[i].fullName());
+    }
+
+    // Test throw NoSuchTagException
+    doThrow(new NoSuchTagException("mock error"))
+        .when(tagManager)
+        .listMetadataObjectsForTag(metalake, "tag1");
+
+    Response response1 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .path("objects")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response1.getStatus());
+
+    ErrorResponse errorResponse = response1.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResponse.getCode());
+    Assertions.assertEquals(NoSuchTagException.class.getSimpleName(), 
errorResponse.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(tagManager)
+        .listMetadataObjectsForTag(any(), any());
+
+    Response response2 =
+        target(tagPath(metalake))
+            .path("tag1")
+            .path("objects")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response2.getStatus());
+
+    ErrorResponse errorResponse1 = response2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResponse1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse1.getType());
+  }
+
+  private String tagPath(String metalake) {
+    return "/metalakes/" + metalake + "/tags";
+  }
+}


Reply via email to