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

mchades 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 a68e5e22ad [#5817] core(feat): Add server-side REST APIs for model 
management (#5948)
a68e5e22ad is described below

commit a68e5e22addb28448b91ddeb717d52bf7d9f74e0
Author: Jerry Shao <[email protected]>
AuthorDate: Thu Dec 26 15:30:55 2024 +0800

    [#5817] core(feat): Add server-side REST APIs for model management (#5948)
    
    ### What changes were proposed in this pull request?
    
    This PR adds the server-side REST endpoint for model management.
    
    ### Why are the changes needed?
    
    This is a part of model management for Gravitino.
    
    Fix: #5817
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Add UTs for this PR.
---
 .../org/apache/gravitino/model/ModelCatalog.java   |   8 +-
 .../gravitino/catalog/model/ModelCatalogImpl.java  |   3 +-
 .../services/org.apache.gravitino.CatalogProvider  |   2 +-
 .../org/apache/gravitino/dto/model/ModelDTO.java   | 163 ++++
 .../gravitino/dto/model/ModelVersionDTO.java       | 184 +++++
 .../dto/requests/CatalogCreateRequest.java         |  31 +-
 .../dto/requests/ModelRegisterRequest.java         |  54 ++
 .../dto/requests/ModelVersionLinkRequest.java      |  67 ++
 .../gravitino/dto/responses/ModelResponse.java     |  59 ++
 .../dto/responses/ModelVersionListResponse.java    |  57 ++
 .../dto/responses/ModelVersionResponse.java        |  59 ++
 .../apache/gravitino/dto/util/DTOConverters.java   |  37 +
 .../apache/gravitino/dto/model/TestModelDTO.java   |  81 ++
 .../gravitino/dto/model/TestModelVersionDTO.java   | 133 ++++
 .../dto/requests/TestCatalogCreateRequest.java     |  15 +-
 .../dto/requests/TestModelRegisterRequest.java     |  47 ++
 .../dto/requests/TestModelVersionLinkRequest.java  |  75 ++
 .../gravitino/dto/responses/TestResponses.java     |  76 ++
 docs/open-api/models.yaml                          | 561 ++++++++++++++
 docs/open-api/openapi.yaml                         |  23 +
 .../apache/gravitino/server/GravitinoServer.java   |   2 +
 .../server/web/rest/ExceptionHandlers.java         |  45 ++
 .../gravitino/server/web/rest/ModelOperations.java | 411 ++++++++++
 .../gravitino/server/web/rest/OperationType.java   |   3 +
 .../server/web/rest/TestModelOperations.java       | 843 +++++++++++++++++++++
 25 files changed, 3006 insertions(+), 33 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java 
b/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java
index cea2e94e3c..3fb39c18ae 100644
--- a/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java
+++ b/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java
@@ -93,7 +93,10 @@ public interface ModelCatalog {
    *
    * @param ident The name identifier of the model.
    * @param uri The model artifact URI.
-   * @param aliases The aliases of the model version. The alias are optional 
and can be empty.
+   * @param aliases The aliases of the model version. The aliases should be 
unique in this model,
+   *     otherwise the {@link ModelVersionAliasesAlreadyExistException} will 
be thrown. The aliases
+   *     are optional and can be empty. Also, be aware that the alias cannot 
be a number or a number
+   *     string.
    * @param comment The comment of the model. The comment is optional and can 
be null.
    * @param properties The properties of the model. The properties are 
optional and can be null or
    *     empty.
@@ -198,7 +201,8 @@ public interface ModelCatalog {
    * @param uri The URI of the model version artifact.
    * @param aliases The aliases of the model version. The aliases should be 
unique in this model,
    *     otherwise the {@link ModelVersionAliasesAlreadyExistException} will 
be thrown. The aliases
-   *     are optional and can be empty.
+   *     are optional and can be empty. Also, be aware that the alias cannot 
be a number or a number
+   *     string.
    * @param comment The comment of the model version. The comment is optional 
and can be null.
    * @param properties The properties of the model version. The properties are 
optional and can be
    *     null or empty.
diff --git 
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
 
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
index 5b90eab726..545f6482a3 100644
--- 
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
+++ 
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
@@ -19,7 +19,6 @@
 package org.apache.gravitino.catalog.model;
 
 import java.util.Map;
-import org.apache.gravitino.CatalogProvider;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.connector.BaseCatalog;
@@ -40,7 +39,7 @@ public class ModelCatalogImpl extends 
BaseCatalog<ModelCatalogImpl> {
 
   @Override
   public String shortName() {
-    return CatalogProvider.shortNameForManagedCatalog(super.type());
+    return "model";
   }
 
   @Override
diff --git 
a/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
 
b/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
index 37c682aa74..e43f995ea7 100644
--- 
a/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
+++ 
b/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
@@ -16,4 +16,4 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-org.apache.gravitino.catalog.model.ModelCatalog
+org.apache.gravitino.catalog.model.ModelCatalogImpl
diff --git a/common/src/main/java/org/apache/gravitino/dto/model/ModelDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/model/ModelDTO.java
new file mode 100644
index 0000000000..4468839933
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/model/ModelDTO.java
@@ -0,0 +1,163 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.model.Model;
+
+/** Represents a model DTO (Data Transfer Object). */
+@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@EqualsAndHashCode
+public class ModelDTO implements Model {
+
+  @JsonProperty("name")
+  private String name;
+
+  @JsonProperty("comment")
+  private String comment;
+
+  @JsonProperty("properties")
+  private Map<String, String> properties;
+
+  @JsonProperty("latestVersion")
+  private int latestVersion;
+
+  @JsonProperty("audit")
+  private AuditDTO audit;
+
+  @Override
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public String comment() {
+    return comment;
+  }
+
+  @Override
+  public Map<String, String> properties() {
+    return properties;
+  }
+
+  @Override
+  public int latestVersion() {
+    return latestVersion;
+  }
+
+  @Override
+  public AuditDTO auditInfo() {
+    return audit;
+  }
+
+  /**
+   * Creates a new builder for constructing a Model DTO.
+   *
+   * @return The builder.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for constructing a Model DTO. */
+  public static class Builder {
+    private String name;
+    private String comment;
+    private Map<String, String> properties;
+    private int latestVersion;
+    private AuditDTO audit;
+
+    /**
+     * Sets the name of the model.
+     *
+     * @param name The name of the model.
+     * @return The builder.
+     */
+    public Builder withName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    /**
+     * Sets the comment associated with the model.
+     *
+     * @param comment The comment associated with the model.
+     * @return The builder.
+     */
+    public Builder withComment(String comment) {
+      this.comment = comment;
+      return this;
+    }
+
+    /**
+     * Sets the properties associated with the model.
+     *
+     * @param properties The properties associated with the model.
+     * @return The builder.
+     */
+    public Builder withProperties(Map<String, String> properties) {
+      this.properties = properties;
+      return this;
+    }
+
+    /**
+     * Sets the latest version of the model.
+     *
+     * @param latestVersion The latest version of the model.
+     * @return The builder.
+     */
+    public Builder withLatestVersion(int latestVersion) {
+      this.latestVersion = latestVersion;
+      return this;
+    }
+
+    /**
+     * Sets the audit information associated with the model.
+     *
+     * @param audit The audit information associated with the model.
+     * @return The builder.
+     */
+    public Builder withAudit(AuditDTO audit) {
+      this.audit = audit;
+      return this;
+    }
+
+    /**
+     * Builds the model DTO.
+     *
+     * @return The model DTO.
+     */
+    public ModelDTO build() {
+      Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot 
be null or empty");
+      Preconditions.checkArgument(latestVersion >= 0, "latestVersion cannot be 
negative");
+      Preconditions.checkArgument(audit != null, "audit cannot be null");
+
+      return new ModelDTO(name, comment, properties, latestVersion, audit);
+    }
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/model/ModelVersionDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/model/ModelVersionDTO.java
new file mode 100644
index 0000000000..e887ba5bdb
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/model/ModelVersionDTO.java
@@ -0,0 +1,184 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.model.ModelVersion;
+
+/** Represents a model version DTO (Data Transfer Object). */
+@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@EqualsAndHashCode
+public class ModelVersionDTO implements ModelVersion {
+
+  @JsonProperty("version")
+  private int version;
+
+  @JsonProperty("comment")
+  private String comment;
+
+  @JsonProperty("aliases")
+  private String[] aliases;
+
+  @JsonProperty("uri")
+  private String uri;
+
+  @JsonProperty("properties")
+  private Map<String, String> properties;
+
+  @JsonProperty("audit")
+  private AuditDTO audit;
+
+  @Override
+  public Audit auditInfo() {
+    return audit;
+  }
+
+  @Override
+  public int version() {
+    return version;
+  }
+
+  @Override
+  public String comment() {
+    return comment;
+  }
+
+  @Override
+  public String[] aliases() {
+    return aliases;
+  }
+
+  @Override
+  public String uri() {
+    return uri;
+  }
+
+  @Override
+  public Map<String, String> properties() {
+    return properties;
+  }
+
+  /**
+   * Creates a new builder for constructing a Model Version DTO.
+   *
+   * @return The builder.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for constructing a Model Version DTO. */
+  public static class Builder {
+    private int version;
+    private String comment;
+    private String[] aliases;
+    private String uri;
+    private Map<String, String> properties;
+    private AuditDTO audit;
+
+    /**
+     * Sets the version number of the model version.
+     *
+     * @param version The version number.
+     * @return The builder.
+     */
+    public Builder withVersion(int version) {
+      this.version = version;
+      return this;
+    }
+
+    /**
+     * Sets the comment of the model version.
+     *
+     * @param comment The comment.
+     * @return The builder.
+     */
+    public Builder withComment(String comment) {
+      this.comment = comment;
+      return this;
+    }
+
+    /**
+     * Sets the aliases of the model version.
+     *
+     * @param aliases The aliases.
+     * @return The builder.
+     */
+    public Builder withAliases(String[] aliases) {
+      this.aliases = aliases;
+      return this;
+    }
+
+    /**
+     * Sets the URI of the model version.
+     *
+     * @param uri The URI.
+     * @return The builder.
+     */
+    public Builder withUri(String uri) {
+      this.uri = uri;
+      return this;
+    }
+
+    /**
+     * Sets the properties of the model version.
+     *
+     * @param properties The properties.
+     * @return The builder.
+     */
+    public Builder withProperties(Map<String, String> properties) {
+      this.properties = properties;
+      return this;
+    }
+
+    /**
+     * Sets the audit information of the model version.
+     *
+     * @param audit The audit information.
+     * @return The builder.
+     */
+    public Builder withAudit(AuditDTO audit) {
+      this.audit = audit;
+      return this;
+    }
+
+    /**
+     * Builds the Model Version DTO.
+     *
+     * @return The Model Version DTO.
+     */
+    public ModelVersionDTO build() {
+      Preconditions.checkArgument(version >= 0, "Version must be 
non-negative");
+      Preconditions.checkArgument(StringUtils.isNotBlank(uri), "URI cannot be 
null or empty");
+      Preconditions.checkArgument(audit != null, "Audit cannot be null");
+
+      return new ModelVersionDTO(version, comment, aliases, uri, properties, 
audit);
+    }
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
index 3da6579676..d543ddb164 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
@@ -18,8 +18,8 @@
  */
 package org.apache.gravitino.dto.requests;
 
+import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonSetter;
 import com.google.common.base.Preconditions;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -54,11 +54,6 @@ public class CatalogCreateRequest implements RESTRequest {
   @JsonProperty("properties")
   private final Map<String, String> properties;
 
-  /** Default constructor for CatalogCreateRequest. */
-  public CatalogCreateRequest() {
-    this(null, null, null, null, null);
-  }
-
   /**
    * Constructor for CatalogCreateRequest.
    *
@@ -68,34 +63,24 @@ public class CatalogCreateRequest implements RESTRequest {
    * @param comment The comment for the catalog.
    * @param properties The properties for the catalog.
    */
+  @JsonCreator
   public CatalogCreateRequest(
-      String name,
-      Catalog.Type type,
-      String provider,
-      String comment,
-      Map<String, String> properties) {
+      @JsonProperty("name") String name,
+      @JsonProperty("type") Catalog.Type type,
+      @JsonProperty("provider") String provider,
+      @JsonProperty("comment") String comment,
+      @JsonProperty("properties") Map<String, String> properties) {
     this.name = name;
     this.type = type;
-    this.provider = provider;
     this.comment = comment;
     this.properties = properties;
-  }
 
-  /**
-   * Sets the provider of the catalog if it is null. The value of provider in 
the request can be
-   * null if the catalog is a managed catalog. For such request, the value 
will be set when it is
-   * deserialized.
-   *
-   * @param provider The provider of the catalog.
-   */
-  @JsonSetter(value = "provider")
-  public void setProvider(String provider) {
     if (StringUtils.isNotBlank(provider)) {
       this.provider = provider;
     } else if (type != null && type.supportsManagedCatalog()) {
       this.provider = CatalogProvider.shortNameForManagedCatalog(type);
     } else {
-      throw new IllegalStateException(
+      throw new IllegalArgumentException(
           "Provider cannot be null for catalog type "
               + type
               + " that doesn't support managed catalog");
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelRegisterRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelRegisterRequest.java
new file mode 100644
index 0000000000..b9cd191617
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelRegisterRequest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to register a model. */
+@Getter
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor
+@AllArgsConstructor
+public class ModelRegisterRequest implements RESTRequest {
+
+  @JsonProperty("name")
+  private String name;
+
+  @JsonProperty("comment")
+  private String comment;
+
+  @JsonProperty("properties")
+  private Map<String, String> properties;
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(name), "\"name\" field is required and cannot 
be empty");
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionLinkRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionLinkRequest.java
new file mode 100644
index 0000000000..24e5932932
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionLinkRequest.java
@@ -0,0 +1,67 @@
+/*
+ * 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 lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to link a model version. */
+@Getter
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor
+@AllArgsConstructor
+public class ModelVersionLinkRequest implements RESTRequest {
+
+  @JsonProperty("uri")
+  private String uri;
+
+  @JsonProperty("aliases")
+  private String[] aliases;
+
+  @JsonProperty("comment")
+  private String comment;
+
+  @JsonProperty("properties")
+  private Map<String, String> properties;
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(uri), "\"uri\" field is required and cannot be 
empty");
+
+    if (aliases != null && aliases.length > 0) {
+      for (String alias : aliases) {
+        Preconditions.checkArgument(
+            StringUtils.isNotBlank(alias), "alias must not be null or empty");
+        Preconditions.checkArgument(
+            !NumberUtils.isCreatable(alias), "alias must not be a number or a 
number string");
+      }
+    }
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/ModelResponse.java 
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelResponse.java
new file mode 100644
index 0000000000..ac51cdd647
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/responses/ModelResponse.java
@@ -0,0 +1,59 @@
+/*
+ * 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.model.ModelDTO;
+
+/** Response for model response. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class ModelResponse extends BaseResponse {
+
+  @JsonProperty("model")
+  private final ModelDTO model;
+
+  /**
+   * Constructor for ModelResponse.
+   *
+   * @param model The model DTO object.
+   */
+  public ModelResponse(ModelDTO model) {
+    super(0);
+    this.model = model;
+  }
+
+  /** Default constructor for ModelResponse. (Used for Jackson 
deserialization.) */
+  public ModelResponse() {
+    super();
+    this.model = null;
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+
+    Preconditions.checkArgument(model != null, "model must not be null");
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionListResponse.java
 
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionListResponse.java
new file mode 100644
index 0000000000..4d3551e139
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionListResponse.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.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+/** Represents a response for a list of model versions. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class ModelVersionListResponse extends BaseResponse {
+
+  @JsonProperty("versions")
+  private int[] versions;
+
+  /**
+   * Constructor for ModelVersionListResponse.
+   *
+   * @param versions The list of model versions.
+   */
+  public ModelVersionListResponse(int[] versions) {
+    super(0);
+    this.versions = versions;
+  }
+
+  /** Default constructor for ModelVersionListResponse. (Used for Jackson 
deserialization.) */
+  public ModelVersionListResponse() {
+    super();
+    this.versions = null;
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+    Preconditions.checkArgument(versions != null, "versions cannot be null");
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionResponse.java
 
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionResponse.java
new file mode 100644
index 0000000000..8b21472833
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionResponse.java
@@ -0,0 +1,59 @@
+/*
+ * 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.model.ModelVersionDTO;
+
+/** Represents a response for a model version. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class ModelVersionResponse extends BaseResponse {
+
+  @JsonProperty("modelVersion")
+  private final ModelVersionDTO modelVersion;
+
+  /**
+   * Constructor for ModelVersionResponse.
+   *
+   * @param modelVersion The model version DTO object.
+   */
+  public ModelVersionResponse(ModelVersionDTO modelVersion) {
+    super(0);
+    this.modelVersion = modelVersion;
+  }
+
+  /** Default constructor for ModelVersionResponse. (Used for Jackson 
deserialization.) */
+  public ModelVersionResponse() {
+    super();
+    this.modelVersion = null;
+  }
+
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+
+    Preconditions.checkArgument(modelVersion != null, "modelVersion must not 
be null");
+  }
+}
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 254de8c324..ce63398e60 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
@@ -51,6 +51,8 @@ import org.apache.gravitino.dto.authorization.UserDTO;
 import org.apache.gravitino.dto.credential.CredentialDTO;
 import org.apache.gravitino.dto.file.FilesetDTO;
 import org.apache.gravitino.dto.messaging.TopicDTO;
+import org.apache.gravitino.dto.model.ModelDTO;
+import org.apache.gravitino.dto.model.ModelVersionDTO;
 import org.apache.gravitino.dto.rel.ColumnDTO;
 import org.apache.gravitino.dto.rel.DistributionDTO;
 import org.apache.gravitino.dto.rel.SortOrderDTO;
@@ -80,6 +82,8 @@ 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.model.Model;
+import org.apache.gravitino.model.ModelVersion;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.expressions.Expression;
@@ -629,6 +633,39 @@ public class DTOConverters {
         .build();
   }
 
+  /**
+   * Converts a Model to a ModelDTO.
+   *
+   * @param model The model to be converted.
+   * @return The model DTO.
+   */
+  public static ModelDTO toDTO(Model model) {
+    return ModelDTO.builder()
+        .withName(model.name())
+        .withComment(model.comment())
+        .withProperties(model.properties())
+        .withLatestVersion(model.latestVersion())
+        .withAudit(toDTO(model.auditInfo()))
+        .build();
+  }
+
+  /**
+   * Converts a ModelVersion to a ModelVersionDTO.
+   *
+   * @param modelVersion The model version to be converted.
+   * @return The model version DTO.
+   */
+  public static ModelVersionDTO toDTO(ModelVersion modelVersion) {
+    return ModelVersionDTO.builder()
+        .withVersion(modelVersion.version())
+        .withComment(modelVersion.comment())
+        .withAliases(modelVersion.aliases())
+        .withUri(modelVersion.uri())
+        .withProperties(modelVersion.properties())
+        .withAudit(toDTO(modelVersion.auditInfo()))
+        .build();
+  }
+
   /**
    * Converts an array of Columns to an array of ColumnDTOs.
    *
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/model/TestModelDTO.java 
b/common/src/test/java/org/apache/gravitino/dto/model/TestModelDTO.java
new file mode 100644
index 0000000000..39e4628eca
--- /dev/null
+++ b/common/src/test/java/org/apache/gravitino/dto/model/TestModelDTO.java
@@ -0,0 +1,81 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+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 TestModelDTO {
+
+  @Test
+  public void testModelSerDe() throws JsonProcessingException {
+    AuditDTO audit = 
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+    Map<String, String> props = ImmutableMap.of("key", "value");
+
+    ModelDTO modelDTO =
+        ModelDTO.builder()
+            .withName("model_test")
+            .withComment("model comment")
+            .withLatestVersion(0)
+            .withProperties(props)
+            .withAudit(audit)
+            .build();
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(modelDTO);
+    ModelDTO deserModelDTO = JsonUtils.objectMapper().readValue(serJson, 
ModelDTO.class);
+    Assertions.assertEquals(modelDTO, deserModelDTO);
+
+    // Test with null comment and properties
+    ModelDTO modelDTO1 =
+        
ModelDTO.builder().withName("model_test").withLatestVersion(0).withAudit(audit).build();
+
+    String serJson1 = JsonUtils.objectMapper().writeValueAsString(modelDTO1);
+    ModelDTO deserModelDTO1 = JsonUtils.objectMapper().readValue(serJson1, 
ModelDTO.class);
+    Assertions.assertEquals(modelDTO1, deserModelDTO1);
+    Assertions.assertNull(deserModelDTO1.comment());
+    Assertions.assertNull(deserModelDTO1.properties());
+  }
+
+  @Test
+  public void testInvalidModelDTO() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelDTO.builder().build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          
ModelDTO.builder().withName("model_test").withLatestVersion(-1).build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          
ModelDTO.builder().withName("model_test").withLatestVersion(0).build();
+        });
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/model/TestModelVersionDTO.java 
b/common/src/test/java/org/apache/gravitino/dto/model/TestModelVersionDTO.java
new file mode 100644
index 0000000000..5251246c37
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/model/TestModelVersionDTO.java
@@ -0,0 +1,133 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+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 TestModelVersionDTO {
+
+  @Test
+  public void testModelVersionSerDe() throws JsonProcessingException {
+    AuditDTO audit = 
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+    Map<String, String> props = ImmutableMap.of("key", "value");
+
+    ModelVersionDTO modelVersionDTO =
+        ModelVersionDTO.builder()
+            .withVersion(0)
+            .withComment("model version comment")
+            .withAliases(new String[] {"alias1", "alias2"})
+            .withUri("uri")
+            .withProperties(props)
+            .withAudit(audit)
+            .build();
+
+    String serJson = 
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO);
+    ModelVersionDTO deserModelVersionDTO =
+        JsonUtils.objectMapper().readValue(serJson, ModelVersionDTO.class);
+
+    Assertions.assertEquals(modelVersionDTO, deserModelVersionDTO);
+
+    // Test with null aliases
+    ModelVersionDTO modelVersionDTO1 =
+        ModelVersionDTO.builder()
+            .withVersion(0)
+            .withComment("model version comment")
+            .withUri("uri")
+            .withProperties(props)
+            .withAudit(audit)
+            .build();
+
+    String serJson1 = 
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO1);
+    ModelVersionDTO deserModelVersionDTO1 =
+        JsonUtils.objectMapper().readValue(serJson1, ModelVersionDTO.class);
+
+    Assertions.assertEquals(modelVersionDTO1, deserModelVersionDTO1);
+    Assertions.assertNull(deserModelVersionDTO1.aliases());
+
+    // Test with empty aliases
+    ModelVersionDTO modelVersionDTO2 =
+        ModelVersionDTO.builder()
+            .withVersion(0)
+            .withComment("model version comment")
+            .withAliases(new String[] {})
+            .withUri("uri")
+            .withProperties(props)
+            .withAudit(audit)
+            .build();
+
+    String serJson2 = 
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO2);
+    ModelVersionDTO deserModelVersionDTO2 =
+        JsonUtils.objectMapper().readValue(serJson2, ModelVersionDTO.class);
+
+    Assertions.assertEquals(modelVersionDTO2, deserModelVersionDTO2);
+    Assertions.assertArrayEquals(new String[] {}, 
deserModelVersionDTO2.aliases());
+
+    // Test with null comment and properties
+    ModelVersionDTO modelVersionDTO3 =
+        
ModelVersionDTO.builder().withVersion(0).withUri("uri").withAudit(audit).build();
+
+    String serJson3 = 
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO3);
+    ModelVersionDTO deserModelVersionDTO3 =
+        JsonUtils.objectMapper().readValue(serJson3, ModelVersionDTO.class);
+
+    Assertions.assertEquals(modelVersionDTO3, deserModelVersionDTO3);
+    Assertions.assertNull(deserModelVersionDTO3.comment());
+    Assertions.assertNull(deserModelVersionDTO3.properties());
+  }
+
+  @Test
+  public void testInvalidModelVersionDTO() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelVersionDTO.builder().build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelVersionDTO.builder().withVersion(-1).build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelVersionDTO.builder().withVersion(0).build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelVersionDTO.builder().withVersion(0).withUri("").build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelVersionDTO.builder().withVersion(0).withUri("uri").build();
+        });
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
index b4b7383a7e..3b5221ff1a 100644
--- 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
@@ -57,19 +57,24 @@ public class TestCatalogCreateRequest {
     String serJson1 = JsonUtils.objectMapper().writeValueAsString(request1);
     CatalogCreateRequest deserRequest1 =
         JsonUtils.objectMapper().readValue(serJson1, 
CatalogCreateRequest.class);
-
     Assertions.assertEquals(
         deserRequest1.getType().name().toLowerCase(Locale.ROOT), 
deserRequest1.getProvider());
     Assertions.assertNull(deserRequest1.getComment());
     Assertions.assertNull(deserRequest1.getProperties());
 
+    String json = "{\"name\":\"catalog_test\",\"type\":\"model\"}";
+    CatalogCreateRequest deserRequest2 =
+        JsonUtils.objectMapper().readValue(json, CatalogCreateRequest.class);
+    Assertions.assertEquals("model", deserRequest2.getProvider());
+
     // Test using null provider with catalog type doesn't support managed 
catalog
-    CatalogCreateRequest request2 =
-        new CatalogCreateRequest("catalog_test", Catalog.Type.RELATIONAL, 
null, null, null);
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> new CatalogCreateRequest("catalog_test", 
Catalog.Type.RELATIONAL, null, null, null));
 
-    String serJson2 = JsonUtils.objectMapper().writeValueAsString(request2);
+    String json1 = "{\"name\":\"catalog_test\",\"type\":\"relational\"}";
     Assertions.assertThrows(
         JsonMappingException.class,
-        () -> JsonUtils.objectMapper().readValue(serJson2, 
CatalogCreateRequest.class));
+        () -> JsonUtils.objectMapper().readValue(json1, 
CatalogCreateRequest.class));
   }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestModelRegisterRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelRegisterRequest.java
new file mode 100644
index 0000000000..09dbdb8c31
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelRegisterRequest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.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 TestModelRegisterRequest {
+
+  @Test
+  public void testModelRegisterRequestSerDe() throws JsonProcessingException {
+    Map<String, String> props = ImmutableMap.of("key", "value");
+    ModelRegisterRequest req = new ModelRegisterRequest("model", "comment", 
props);
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(req);
+    ModelRegisterRequest deserReq =
+        JsonUtils.objectMapper().readValue(serJson, 
ModelRegisterRequest.class);
+    Assertions.assertEquals(req, deserReq);
+
+    // Test with null comment and properties
+    ModelRegisterRequest req1 = new ModelRegisterRequest("model", null, null);
+    String serJson1 = JsonUtils.objectMapper().writeValueAsString(req1);
+    ModelRegisterRequest deserReq1 =
+        JsonUtils.objectMapper().readValue(serJson1, 
ModelRegisterRequest.class);
+    Assertions.assertEquals(req1, deserReq1);
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestModelVersionLinkRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelVersionLinkRequest.java
new file mode 100644
index 0000000000..4c0df6d73e
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelVersionLinkRequest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.requests;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import 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 TestModelVersionLinkRequest {
+
+  @Test
+  public void testModelVersionLinkRequestSerDe() throws 
JsonProcessingException {
+    Map<String, String> props = ImmutableMap.of("key", "value");
+    ModelVersionLinkRequest request =
+        new ModelVersionLinkRequest("uri", new String[] {"alias1", "alias2"}, 
"comment", props);
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+    ModelVersionLinkRequest deserRequest =
+        JsonUtils.objectMapper().readValue(serJson, 
ModelVersionLinkRequest.class);
+
+    Assertions.assertEquals(request, deserRequest);
+
+    // Test with null aliases
+    ModelVersionLinkRequest request1 = new ModelVersionLinkRequest("uri", 
null, "comment", props);
+
+    String serJson1 = JsonUtils.objectMapper().writeValueAsString(request1);
+    ModelVersionLinkRequest deserRequest1 =
+        JsonUtils.objectMapper().readValue(serJson1, 
ModelVersionLinkRequest.class);
+
+    Assertions.assertEquals(request1, deserRequest1);
+    Assertions.assertNull(deserRequest1.getAliases());
+
+    // Test with empty aliases
+    ModelVersionLinkRequest request2 =
+        new ModelVersionLinkRequest("uri", new String[] {}, "comment", props);
+
+    String serJson2 = JsonUtils.objectMapper().writeValueAsString(request2);
+    ModelVersionLinkRequest deserRequest2 =
+        JsonUtils.objectMapper().readValue(serJson2, 
ModelVersionLinkRequest.class);
+
+    Assertions.assertEquals(request2, deserRequest2);
+    Assertions.assertEquals(0, deserRequest2.getAliases().length);
+
+    // Test with null comment and properties
+    ModelVersionLinkRequest request3 =
+        new ModelVersionLinkRequest("uri", new String[] {"alias1", "alias2"}, 
null, null);
+
+    String serJson3 = JsonUtils.objectMapper().writeValueAsString(request3);
+    ModelVersionLinkRequest deserRequest3 =
+        JsonUtils.objectMapper().readValue(serJson3, 
ModelVersionLinkRequest.class);
+
+    Assertions.assertEquals(request3, deserRequest3);
+    Assertions.assertNull(deserRequest3.getComment());
+    Assertions.assertNull(deserRequest3.getProperties());
+  }
+}
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 5f94782022..57813c0bc6 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
@@ -26,8 +26,10 @@ 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.ImmutableMap;
 import com.google.common.collect.Lists;
 import java.time.Instant;
+import java.util.Map;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.Privileges;
@@ -41,6 +43,8 @@ import org.apache.gravitino.dto.authorization.GroupDTO;
 import org.apache.gravitino.dto.authorization.RoleDTO;
 import org.apache.gravitino.dto.authorization.SecurableObjectDTO;
 import org.apache.gravitino.dto.authorization.UserDTO;
+import org.apache.gravitino.dto.model.ModelDTO;
+import org.apache.gravitino.dto.model.ModelVersionDTO;
 import org.apache.gravitino.dto.rel.ColumnDTO;
 import org.apache.gravitino.dto.rel.TableDTO;
 import org.apache.gravitino.dto.rel.partitioning.Partitioning;
@@ -390,4 +394,76 @@ public class TestResponses {
     FileLocationResponse response = new FileLocationResponse();
     assertThrows(IllegalArgumentException.class, () -> response.validate());
   }
+
+  @Test
+  void testModelResponse() throws JsonProcessingException {
+    Map<String, String> props = ImmutableMap.of("key", "value");
+    AuditDTO audit = 
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+    ModelDTO modelDTO =
+        ModelDTO.builder()
+            .withName("model1")
+            .withLatestVersion(0)
+            .withComment("comment1")
+            .withProperties(props)
+            .withAudit(audit)
+            .build();
+
+    ModelResponse response = new ModelResponse(modelDTO);
+    String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+    ModelResponse deserResponse = JsonUtils.objectMapper().readValue(serJson, 
ModelResponse.class);
+
+    assertEquals(response, deserResponse);
+
+    ModelResponse response1 = new ModelResponse();
+    assertThrows(IllegalArgumentException.class, response1::validate);
+  }
+
+  @Test
+  void testModelVersionListResponse() throws JsonProcessingException {
+    ModelVersionListResponse response1 = new ModelVersionListResponse(new 
int[] {});
+    assertDoesNotThrow(response1::validate);
+
+    String serJson1 = JsonUtils.objectMapper().writeValueAsString(response1);
+    ModelVersionListResponse deserResponse1 =
+        JsonUtils.objectMapper().readValue(serJson1, 
ModelVersionListResponse.class);
+    assertEquals(response1, deserResponse1);
+    assertArrayEquals(new int[] {}, deserResponse1.getVersions());
+
+    ModelVersionListResponse response2 = new ModelVersionListResponse(new 
int[] {1, 2});
+    assertDoesNotThrow(response2::validate);
+
+    String serJson2 = JsonUtils.objectMapper().writeValueAsString(response2);
+    ModelVersionListResponse deserResponse2 =
+        JsonUtils.objectMapper().readValue(serJson2, 
ModelVersionListResponse.class);
+    assertEquals(response2, deserResponse2);
+    assertArrayEquals(new int[] {1, 2}, deserResponse2.getVersions());
+  }
+
+  @Test
+  void testModelVersionResponse() throws JsonProcessingException {
+    Map<String, String> props = ImmutableMap.of("key", "value");
+    AuditDTO audit = 
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+    ModelVersionDTO modelVersionDTO =
+        ModelVersionDTO.builder()
+            .withVersion(0)
+            .withComment("model version comment")
+            .withAliases(new String[] {"alias1", "alias2"})
+            .withUri("uri")
+            .withProperties(props)
+            .withAudit(audit)
+            .build();
+
+    ModelVersionResponse response = new ModelVersionResponse(modelVersionDTO);
+    response.validate(); // No exception thrown
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+    ModelVersionResponse deserResponse =
+        JsonUtils.objectMapper().readValue(serJson, 
ModelVersionResponse.class);
+    assertEquals(response, deserResponse);
+
+    ModelVersionResponse response1 = new ModelVersionResponse();
+    assertThrows(IllegalArgumentException.class, response1::validate);
+  }
 }
diff --git a/docs/open-api/models.yaml b/docs/open-api/models.yaml
new file mode 100644
index 0000000000..713a7037cd
--- /dev/null
+++ b/docs/open-api/models.yaml
@@ -0,0 +1,561 @@
+# 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.
+
+---
+
+paths:
+
+  /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models:
+    parameters:
+      - $ref: "./openapi.yaml#/components/parameters/metalake"
+      - $ref: "./openapi.yaml#/components/parameters/catalog"
+      - $ref: "./openapi.yaml#/components/parameters/schema"
+
+    get:
+      tags:
+        - model
+      summary: List models
+      operationId: listModels
+      responses:
+        "200":
+          $ref: "./openapi.yaml#/components/responses/EntityListResponse"
+        "400":
+          $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+    post:
+      tags:
+        - model
+      summary: Register model
+      operationId: registerModel
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/ModelRegisterRequest"
+            examples:
+              ModelRegisterRequest:
+                $ref: "#/components/examples/ModelRegisterRequest"
+      responses:
+        "200":
+          $ref: "#/components/responses/ModelResponse"
+        "409":
+          description: Conflict - The target model already exists
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                ModelAlreadyExistsErrorResponse:
+                  $ref: "#/components/examples/ModelAlreadyExistsException"
+        "404":
+          description: Not Found - The schema does not exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                NoSuchSchemaException:
+                  $ref: 
"./schemas.yaml#/components/examples/NoSuchSchemaException"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+  /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}:
+    parameters:
+      - $ref: "./openapi.yaml#/components/parameters/metalake"
+      - $ref: "./openapi.yaml#/components/parameters/catalog"
+      - $ref: "./openapi.yaml#/components/parameters/schema"
+      - $ref: "./openapi.yaml#/components/parameters/model"
+
+    get:
+      tags:
+        - model
+      summary: Get model
+      operationId: getModel
+      description: Returns the specified model object
+      responses:
+        "200":
+          $ref: "#/components/responses/ModelResponse"
+        "404":
+          description: Not Found - The target fileset does not exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                NoSuchMetalakeException:
+                  $ref: 
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
+                NoSuchCatalogException:
+                  $ref: 
"./catalogs.yaml#/components/examples/NoSuchCatalogException"
+                NoSuchSchemaException:
+                  $ref: 
"./schemas.yaml#/components/examples/NoSuchSchemaException"
+                NoSuchModelException:
+                  $ref: "#/components/examples/NoSuchModelException"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+    delete:
+      tags:
+        - model
+      summary: delete model
+      operationId: deleteModel
+      responses:
+        "200":
+          $ref: "./openapi.yaml#/components/responses/DropResponse"
+        "400":
+          $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+    post:
+      tags:
+        - model
+      summary: link model version
+      operationId: linkModelVersion
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/ModelVersionLinkRequest"
+            examples:
+              ModelVersionLinkRequest:
+                $ref: "#/components/examples/ModelVersionLinkRequest"
+      responses:
+        "200":
+          $ref: "./openapi.yaml#/components/responses/BaseResponse"
+        "404":
+          description: Not Found - The target model does not exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                NoSuchModelException:
+                  $ref: "#/components/examples/NoSuchModelException"
+        "409":
+          description: Conflict - The model version aliases already exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                ModelVersionAliasesAlreadyExistException:
+                  $ref: 
"#/components/examples/ModelVersionAliasesAlreadyExistException"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+  
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions:
+    parameters:
+      - $ref: "./openapi.yaml#/components/parameters/metalake"
+      - $ref: "./openapi.yaml#/components/parameters/catalog"
+      - $ref: "./openapi.yaml#/components/parameters/schema"
+      - $ref: "./openapi.yaml#/components/parameters/model"
+
+    get:
+      tags:
+        - model
+      summary: List model versions
+      operationId: listModelVersions
+      responses:
+        "200":
+          $ref: "#/components/responses/ModelVersionListResponse"
+        "404":
+          description: Not Found - The target model does not exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                NoSuchModelException:
+                  $ref: "#/components/examples/NoSuchModelException"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+  
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions/{version}:
+    parameters:
+      - $ref: "./openapi.yaml#/components/parameters/metalake"
+      - $ref: "./openapi.yaml#/components/parameters/catalog"
+      - $ref: "./openapi.yaml#/components/parameters/schema"
+      - $ref: "./openapi.yaml#/components/parameters/model"
+      - $ref: "#/components/parameters/version"
+
+    get:
+      tags:
+        - model
+      summary: Get model version
+      operationId: getModelVersion
+      responses:
+        "200":
+          $ref: "#/components/responses/ModelVersionResponse"
+        "404":
+          description: Not Found - The target model version does not exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                NoSuchModelVersionException:
+                  $ref: "#/components/examples/NoSuchModelVersionException"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+    delete:
+      tags:
+        - model
+      summary: delete model version
+      operationId: deleteModelVersion
+      responses:
+        "200":
+          $ref: "./openapi.yaml#/components/responses/DropResponse"
+        "400":
+          $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+  
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/aliases/{alias}:
+    parameters:
+      - $ref: "./openapi.yaml#/components/parameters/metalake"
+      - $ref: "./openapi.yaml#/components/parameters/catalog"
+      - $ref: "./openapi.yaml#/components/parameters/schema"
+      - $ref: "./openapi.yaml#/components/parameters/model"
+      - $ref: "#/components/parameters/alias"
+
+    get:
+      tags:
+        - model
+      summary: Get model version by alias
+      operationId: getModelVersionByAlias
+      responses:
+        "200":
+          $ref: "#/components/responses/ModelVersionResponse"
+        "404":
+          description: Not Found - The target model version does not exist
+          content:
+            application/vnd.gravitino.v1+json:
+              schema:
+                $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+              examples:
+                NoSuchModelVersionException:
+                  $ref: "#/components/examples/NoSuchModelVersionException"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+    delete:
+      tags:
+        - model
+      summary: delete model version by alias
+      operationId: deleteModelVersionByAlias
+      responses:
+        "200":
+          $ref: "./openapi.yaml#/components/responses/DropResponse"
+        "400":
+          $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+        "5xx":
+          $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+components:
+  parameters:
+    version:
+      name: version
+      in: path
+      required: true
+      description: The version of the model
+      schema:
+        type: integer
+    alias:
+      name: alias
+      in: path
+      required: true
+      description: The alias of the model version
+      schema:
+        type: string
+
+  schemas:
+    Model:
+      type: object
+      required:
+        - name
+        - audit
+        - latestVersion
+      properties:
+        name:
+          type: string
+          description: The name of the model
+        latestVersion:
+          type: integer
+          description: The latest version of the model
+        comment:
+          type: string
+          description: The comment of the fileset
+          nullable: true
+        properties:
+          type: object
+          description: The properties of the fileset
+          nullable: true
+          default: {}
+          additionalProperties:
+            type: string
+        audit:
+          $ref: "./openapi.yaml#/components/schemas/Audit"
+
+    ModelVersion:
+      type: object
+      required:
+        - uri
+        - version
+        - audit
+      properties:
+        uri:
+          type: string
+          description: The uri of the model version
+        version:
+          type: integer
+          description: The version of the model
+        aliases:
+          type: array
+          description: The aliases of the model version
+          nullable: true
+          items:
+            type: string
+        comment:
+          type: string
+          description: The comment of the model version
+          nullable: true
+        properties:
+          type: object
+          description: The properties of the model version
+          nullable: true
+          default: {}
+          additionalProperties:
+            type: string
+        audit:
+          $ref: "./openapi.yaml#/components/schemas/Audit"
+
+    ModelRegisterRequest:
+      type: object
+      required:
+        - name
+      properties:
+        name:
+          type: string
+          description: The name of the model. Can not be empty.
+        comment:
+          type: string
+          description: The comment of the model. Can be empty.
+          nullable: true
+        properties:
+          type: object
+          description: The properties of the model. Can be empty.
+          nullable: true
+          default: {}
+          additionalProperties:
+            type: string
+
+    ModelVersionLinkRequest:
+      type: object
+      required:
+        - uri
+      properties:
+        uri:
+          type: string
+          description: The uri of the model version
+        aliases:
+          type: array
+          description: The aliases of the model version
+          nullable: true
+          items:
+            type: string
+        comment:
+          type: string
+          description: The comment of the model version
+          nullable: true
+        properties:
+          type: object
+          description: The properties of the model version
+          nullable: true
+          default: {}
+          additionalProperties:
+            type: string
+
+  responses:
+    ModelResponse:
+      description: The response of model object
+      content:
+        application/vnd.gravitino.v1+json:
+          schema:
+            type: object
+            properties:
+              code:
+                type: integer
+                format: int32
+                description: Status code of the response
+                enum:
+                  - 0
+              model:
+                $ref: "#/components/schemas/Model"
+          examples:
+            ModelResponse:
+              $ref: "#/components/examples/ModelResponse"
+    ModelVersionListResponse:
+      description: The response of model version list
+      content:
+        application/vnd.gravitino.v1+json:
+          schema:
+            type: object
+            properties:
+              code:
+                type: integer
+                format: int32
+                description: Status code of the response
+                enum:
+                  - 0
+              versions:
+                type: array
+                description: The list of model versions
+                items:
+                  format: int32
+          examples:
+            ModelVersionListResponse:
+              $ref: "#/components/examples/ModelVersionListResponse"
+    ModelVersionResponse:
+      description: The response of model version object
+      content:
+        application/vnd.gravitino.v1+json:
+          schema:
+            type: object
+            properties:
+              code:
+                type: integer
+                format: int32
+                description: Status code of the response
+                enum:
+                  - 0
+              modelVersion:
+                $ref: "#/components/schemas/ModelVersion"
+          examples:
+            ModelResponse:
+              $ref: "#/components/examples/ModelVersionResponse"
+
+  examples:
+    ModelRegisterRequest:
+      value: {
+        "name": "model1",
+        "comment": "This is a comment",
+        "properties": {
+          "key1": "value1",
+          "key2": "value2"
+        }
+      }
+
+    ModelVersionLinkRequest:
+      value: {
+        "uri": "hdfs://path/to/model",
+        "aliases": ["alias1", "alias2"],
+        "comment": "This is a comment",
+        "properties": {
+          "key1": "value1",
+          "key2": "value2"
+        }
+      }
+
+    ModelResponse:
+      value: {
+        "code": 0,
+        "model" : {
+          "name": "model1",
+          "latestVersion": 0,
+          "comment": "This is a comment",
+          "properties": {
+            "key1": "value1",
+            "key2": "value2"
+          },
+          "audit": {
+            "creator": "user1",
+            "createTime": "2021-01-01T00:00:00Z",
+            "lastModifier": "user1",
+            "lastModifiedTime": "2021-01-01T00:00:00Z"
+          }
+        }
+      }
+
+    ModelVersionListResponse:
+      value: {
+        "code": 0,
+        "versions": [0, 1, 2]
+      }
+
+    ModelVersionResponse:
+      value: {
+        "code": 0,
+        "modelVersion" : {
+          "uri": "hdfs://path/to/model",
+          "version": 0,
+          "aliases": ["alias1", "alias2"],
+          "comment": "This is a comment",
+          "properties": {
+            "key1": "value1",
+            "key2": "value2"
+          },
+          "audit": {
+            "creator": "user1",
+            "createTime": "2021-01-01T00:00:00Z",
+            "lastModifier": "user1",
+            "lastModifiedTime": "2021-01-01T00:00:00Z"
+          }
+        }
+      }
+
+    ModelAlreadyExistsException:
+      value: {
+        "code": 1004,
+        "type": "ModelAlreadyExistsException",
+        "message": "Model already exists",
+        "stack": [
+            "org.apache.gravitino.exceptions.ModelAlreadyExistsException: 
Model already exists"
+        ]
+      }
+
+    NoSuchModelException:
+      value: {
+        "code": 1003,
+        "type": "NoSuchModelException",
+        "message": "Model does not exist",
+        "stack": [
+            "org.apache.gravitino.exceptions.NoSuchModelException: Model does 
not exist"
+        ]
+      }
+
+    ModelVersionAliasesAlreadyExistException:
+      value: {
+        "code": 1004,
+        "type": "ModelVersionAliasesAlreadyExistException",
+        "message": "Model version aliases already exist",
+        "stack": [
+          
"org.apache.gravitino.exceptions.ModelVersionAliasesAlreadyExistException: 
Model version aliases already exist"
+        ]
+      }
+
+    NoSuchModelVersionException:
+      value: {
+        "code": 1003,
+        "type": "NoSuchModelVersionException",
+        "message": "Model version does not exist",
+        "stack": [
+          "org.apache.gravitino.exceptions.NoSuchModelVersionException: Model 
version does not exist"
+        ]
+      }
diff --git a/docs/open-api/openapi.yaml b/docs/open-api/openapi.yaml
index dd0564a7f9..d0c941ab47 100644
--- a/docs/open-api/openapi.yaml
+++ b/docs/open-api/openapi.yaml
@@ -113,6 +113,20 @@ paths:
   /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/topics/{topic}:
     $ref: 
"./topics.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1topics~1%7Btopic%7D"
 
+  /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models:
+    $ref: 
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models"
+
+  /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}:
+    $ref: 
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D"
+
+  
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions:
+    $ref: 
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D~1versions"
+
+  
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions/{version}:
+    $ref: 
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D~1versions~1%7Bversion%7D"
+
+  
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/aliases/{alias}:
+    $ref: 
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D~1aliases~1%7Balias%7D"
 
   /metalakes/{metalake}/users:
     $ref: "./users.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1users"
@@ -430,6 +444,14 @@ components:
       schema:
         type: string
 
+    model:
+      name: model
+      in: path
+      description: The name of the model
+      required: true
+      schema:
+        type: string
+
     tag:
       name: tag
       in: path
@@ -476,6 +498,7 @@ components:
           - "COLUMN"
           - "FILESET"
           - "TOPIC"
+          - "MODEL"
           - "ROLE"
     metadataObjectFullName:
       name: metadataObjectFullName
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 63e53aefd5..2afc65482b 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.Configs;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.catalog.CatalogDispatcher;
 import org.apache.gravitino.catalog.FilesetDispatcher;
+import org.apache.gravitino.catalog.ModelDispatcher;
 import org.apache.gravitino.catalog.PartitionDispatcher;
 import org.apache.gravitino.catalog.SchemaDispatcher;
 import org.apache.gravitino.catalog.TableDispatcher;
@@ -118,6 +119,7 @@ public class GravitinoServer extends ResourceConfig {
             bind(gravitinoEnv.credentialOperationDispatcher())
                 .to(CredentialOperationDispatcher.class)
                 .ranked(1);
+            
bind(gravitinoEnv.modelDispatcher()).to(ModelDispatcher.class).ranked(1);
           }
         });
     register(JsonProcessingExceptionMapper.class);
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 faf94f5064..b71219b045 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
@@ -32,6 +32,8 @@ import org.apache.gravitino.exceptions.InUseException;
 import org.apache.gravitino.exceptions.MetalakeAlreadyExistsException;
 import org.apache.gravitino.exceptions.MetalakeInUseException;
 import org.apache.gravitino.exceptions.MetalakeNotInUseException;
+import org.apache.gravitino.exceptions.ModelAlreadyExistsException;
+import 
org.apache.gravitino.exceptions.ModelVersionAliasesAlreadyExistException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 import org.apache.gravitino.exceptions.NonEmptyCatalogException;
 import org.apache.gravitino.exceptions.NonEmptyMetalakeException;
@@ -126,6 +128,11 @@ public class ExceptionHandlers {
     return CredentialExceptionHandler.INSTANCE.handle(op, metadataObjectName, 
"", e);
   }
 
+  public static Response handleModelException(
+      OperationType op, String model, String schema, Exception e) {
+    return ModelExceptionHandler.INSTANCE.handle(op, model, schema, e);
+  }
+
   public static Response handleTestConnectionException(Exception e) {
     ErrorResponse response;
     if (e instanceof IllegalArgumentException) {
@@ -729,6 +736,44 @@ public class ExceptionHandlers {
     }
   }
 
+  private static class ModelExceptionHandler extends BaseExceptionHandler {
+    private static final ExceptionHandler INSTANCE = new 
ModelExceptionHandler();
+
+    private static String getModelErrorMsg(
+        String model, String operation, String schema, String reason) {
+      return String.format(
+          "Failed to operate model(s)%s operation [%s] under schema [%s], 
reason [%s]",
+          model, operation, schema, reason);
+    }
+
+    @Override
+    public Response handle(OperationType op, String model, String schema, 
Exception e) {
+      String formatted = StringUtil.isBlank(model) ? "" : " [" + model + "]";
+      String errorMsg = getModelErrorMsg(formatted, op.name(), schema, 
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 ModelAlreadyExistsException
+          || e instanceof ModelVersionAliasesAlreadyExistException) {
+        return Utils.alreadyExists(errorMsg, e);
+
+      } else if (e instanceof ForbiddenException) {
+        return Utils.forbidden(errorMsg, e);
+
+      } else if (e instanceof NotInUseException) {
+        return Utils.notInUse(errorMsg, e);
+
+      } else {
+        return super.handle(op, model, schema, e);
+      }
+    }
+  }
+
   @VisibleForTesting
   static class BaseExceptionHandler extends ExceptionHandler {
 
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java
new file mode 100644
index 0000000000..fd50782108
--- /dev/null
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java
@@ -0,0 +1,411 @@
+/*
+ * 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 javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.catalog.ModelDispatcher;
+import org.apache.gravitino.dto.requests.ModelRegisterRequest;
+import org.apache.gravitino.dto.requests.ModelVersionLinkRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.EntityListResponse;
+import org.apache.gravitino.dto.responses.ModelResponse;
+import org.apache.gravitino.dto.responses.ModelVersionListResponse;
+import org.apache.gravitino.dto.responses.ModelVersionResponse;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelVersion;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.NamespaceUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models")
+public class ModelOperations {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ModelOperations.class);
+
+  private final ModelDispatcher modelDispatcher;
+
+  @Context private HttpServletRequest httpRequest;
+
+  @Inject
+  public ModelOperations(ModelDispatcher modelDispatcher) {
+    this.modelDispatcher = modelDispatcher;
+  }
+
+  @GET
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "list-model." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "list-model", absolute = true)
+  public Response listModels(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema) {
+    LOG.info("Received list models request for schema: {}.{}.{}", metalake, 
catalog, schema);
+    Namespace modelNs = NamespaceUtil.ofModel(metalake, catalog, schema);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            NameIdentifier[] modelIds = modelDispatcher.listModels(modelNs);
+            modelIds = modelIds == null ? new NameIdentifier[0] : modelIds;
+            LOG.info("List {} models under schema {}", modelIds.length, 
modelNs);
+            return Utils.ok(new EntityListResponse(modelIds));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(OperationType.LIST, "", 
schema, e);
+    }
+  }
+
+  @GET
+  @Path("{model}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "get-model." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "get-model", absolute = true)
+  public Response getModel(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model) {
+    LOG.info("Received get model request: {}.{}.{}.{}", metalake, catalog, 
schema, model);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            Model m = modelDispatcher.getModel(modelId);
+            LOG.info("Model got: {}", modelId);
+            return Utils.ok(new ModelResponse(DTOConverters.toDTO(m)));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(OperationType.GET, model, 
schema, e);
+    }
+  }
+
+  @POST
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "register-model." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "register-model", absolute = true)
+  public Response registerModel(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      ModelRegisterRequest request) {
+    LOG.info(
+        "Received register model request: {}.{}.{}.{}",
+        metalake,
+        catalog,
+        schema,
+        request.getName());
+
+    try {
+      request.validate();
+      NameIdentifier modelId =
+          NameIdentifierUtil.ofModel(metalake, catalog, schema, 
request.getName());
+
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            Model m =
+                modelDispatcher.registerModel(
+                    modelId, request.getComment(), request.getProperties());
+            LOG.info("Model registered: {}", modelId);
+            return Utils.ok(new ModelResponse(DTOConverters.toDTO(m)));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(
+          OperationType.REGISTER, request.getName(), schema, e);
+    }
+  }
+
+  @DELETE
+  @Path("{model}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "delete-model." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
+  @ResponseMetered(name = "delete-model", absolute = true)
+  public Response deleteModel(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model) {
+    LOG.info("Received delete model request: {}.{}.{}.{}", metalake, catalog, 
schema, model);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            boolean deleted = modelDispatcher.deleteModel(modelId);
+            if (!deleted) {
+              LOG.warn("Cannot find to be deleted model {} under schema {}", 
model, schema);
+            } else {
+              LOG.info("Model deleted: {}", modelId);
+            }
+
+            return Utils.ok(new DropResponse(deleted));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(OperationType.DELETE, 
model, schema, e);
+    }
+  }
+
+  @GET
+  @Path("{model}/versions")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "list-model-versions." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "list-model-versions", absolute = true)
+  public Response listModelVersions(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model) {
+    LOG.info("Received list model versions request: {}.{}.{}.{}", metalake, 
catalog, schema, model);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            int[] versions = modelDispatcher.listModelVersions(modelId);
+            versions = versions == null ? new int[0] : versions;
+            LOG.info("List {} versions of model {}", versions.length, modelId);
+            return Utils.ok(new ModelVersionListResponse(versions));
+          });
+
+    } catch (Exception e) {
+      return 
ExceptionHandlers.handleModelException(OperationType.LIST_VERSIONS, model, 
schema, e);
+    }
+  }
+
+  @GET
+  @Path("{model}/versions/{version}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "get-model-version." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "get-model-version", absolute = true)
+  public Response getModelVersion(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model,
+      @PathParam("version") int version) {
+    LOG.info(
+        "Received get model version request: {}.{}.{}.{}.{}",
+        metalake,
+        catalog,
+        schema,
+        model,
+        version);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            ModelVersion mv = modelDispatcher.getModelVersion(modelId, 
version);
+            LOG.info("Model version got: {}.{}", modelId, version);
+            return Utils.ok(new ModelVersionResponse(DTOConverters.toDTO(mv)));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(
+          OperationType.GET, versionString(model, version), schema, e);
+    }
+  }
+
+  @GET
+  @Path("{model}/aliases/{alias}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "get-model-alias." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "get-model-alias", absolute = true)
+  public Response getModelVersionByAlias(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model,
+      @PathParam("alias") String alias) {
+    LOG.info(
+        "Received get model version alias request: {}.{}.{}.{}.{}",
+        metalake,
+        catalog,
+        schema,
+        model,
+        alias);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            ModelVersion mv = modelDispatcher.getModelVersion(modelId, alias);
+            LOG.info("Model version alias got: {}.{}", modelId, alias);
+            return Utils.ok(new ModelVersionResponse(DTOConverters.toDTO(mv)));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(
+          OperationType.GET, aliasString(model, alias), schema, e);
+    }
+  }
+
+  @POST
+  @Path("{model}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "link-model-version." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "link-model-version", absolute = true)
+  public Response linkModelVersion(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model,
+      ModelVersionLinkRequest request) {
+    LOG.info("Received link model version request: {}.{}.{}.{}", metalake, 
catalog, schema, model);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      request.validate();
+
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            modelDispatcher.linkModelVersion(
+                modelId,
+                request.getUri(),
+                request.getAliases(),
+                request.getComment(),
+                request.getProperties());
+            LOG.info("Model version linked: {}", modelId);
+            return Utils.ok(new BaseResponse());
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(OperationType.LINK, model, 
schema, e);
+    }
+  }
+
+  @DELETE
+  @Path("{model}/versions/{version}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "delete-model-version." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "delete-model-version", absolute = true)
+  public Response deleteModelVersion(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model,
+      @PathParam("version") int version) {
+    LOG.info(
+        "Received delete model version request: {}.{}.{}.{}.{}",
+        metalake,
+        catalog,
+        schema,
+        model,
+        version);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            boolean deleted = modelDispatcher.deleteModelVersion(modelId, 
version);
+            if (!deleted) {
+              LOG.warn("Cannot find to be deleted version {} in model {}", 
version, model);
+            } else {
+              LOG.info("Model version deleted: {}.{}", modelId, version);
+            }
+
+            return Utils.ok(new DropResponse(deleted));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(
+          OperationType.DELETE, versionString(model, version), schema, e);
+    }
+  }
+
+  @DELETE
+  @Path("{model}/aliases/{alias}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "delete-model-alias." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "delete-model-alias", absolute = true)
+  public Response deleteModelVersionByAlias(
+      @PathParam("metalake") String metalake,
+      @PathParam("catalog") String catalog,
+      @PathParam("schema") String schema,
+      @PathParam("model") String model,
+      @PathParam("alias") String alias) {
+    LOG.info(
+        "Received delete model version by alias request: {}.{}.{}.{}.{}",
+        metalake,
+        catalog,
+        schema,
+        model,
+        alias);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, model);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            boolean deleted = modelDispatcher.deleteModelVersion(modelId, 
alias);
+            if (!deleted) {
+              LOG.warn(
+                  "Cannot find to be deleted model version by alias {} in 
model {}", alias, model);
+            } else {
+              LOG.info("Model version by alias deleted: {}.{}", modelId, 
alias);
+            }
+
+            return Utils.ok(new DropResponse(deleted));
+          });
+
+    } catch (Exception e) {
+      return ExceptionHandlers.handleModelException(
+          OperationType.DELETE, aliasString(model, alias), schema, e);
+    }
+  }
+
+  private String versionString(String model, int version) {
+    return model + " version(" + version + ")";
+  }
+
+  private String aliasString(String model, String alias) {
+    return model + " alias(" + alias + ")";
+  }
+}
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 8d4bc322ae..2b8abd91f1 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
@@ -35,4 +35,7 @@ public enum OperationType {
   REVOKE,
   ASSOCIATE,
   SET,
+  REGISTER, // An operation to register a model
+  LIST_VERSIONS, // An operation to list versions of a model
+  LINK // An operation to link a version to a model
 }
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
new file mode 100644
index 0000000000..42e48d0302
--- /dev/null
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
@@ -0,0 +1,843 @@
+/*
+ * 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.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Map;
+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.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.catalog.ModelDispatcher;
+import org.apache.gravitino.dto.requests.ModelRegisterRequest;
+import org.apache.gravitino.dto.requests.ModelVersionLinkRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.EntityListResponse;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.ModelResponse;
+import org.apache.gravitino.dto.responses.ModelVersionListResponse;
+import org.apache.gravitino.dto.responses.ModelVersionResponse;
+import org.apache.gravitino.exceptions.ModelAlreadyExistsException;
+import org.apache.gravitino.exceptions.NoSuchModelException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelVersion;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.NamespaceUtil;
+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 TestModelOperations 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 ModelDispatcher modelDispatcher = mock(ModelDispatcher.class);
+
+  private AuditInfo testAuditInfo =
+      
AuditInfo.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+  private Map<String, String> properties = ImmutableMap.of("key1", "value");
+
+  private String metalake = "metalake_for_model_test";
+
+  private String catalog = "catalog_for_model_test";
+
+  private String schema = "schema_for_model_test";
+
+  private Namespace modelNs = NamespaceUtil.ofModel(metalake, catalog, schema);
+
+  @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(ModelOperations.class);
+    resourceConfig.register(
+        new AbstractBinder() {
+          @Override
+          protected void configure() {
+            bind(modelDispatcher).to(ModelDispatcher.class).ranked(2);
+            bindFactory(TestModelOperations.MockServletRequestFactory.class)
+                .to(HttpServletRequest.class);
+          }
+        });
+
+    return resourceConfig;
+  }
+
+  @Test
+  public void testListModels() {
+    NameIdentifier modelId1 = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    NameIdentifier modelId2 = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model2");
+    NameIdentifier[] modelIds = new NameIdentifier[] {modelId1, modelId2};
+    when(modelDispatcher.listModels(modelNs)).thenReturn(modelIds);
+
+    Response response =
+        target(modelPath())
+            .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());
+
+    EntityListResponse resp = response.readEntity(EntityListResponse.class);
+    Assertions.assertEquals(0, resp.getCode());
+    Assertions.assertArrayEquals(modelIds, resp.identifiers());
+
+    // Test mock return null for listModels
+    when(modelDispatcher.listModels(modelNs)).thenReturn(null);
+    Response resp1 =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp1.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp1.getMediaType());
+
+    EntityListResponse resp2 = resp1.readEntity(EntityListResponse.class);
+    Assertions.assertEquals(0, resp2.getCode());
+    Assertions.assertEquals(0, resp2.identifiers().length);
+
+    // Test mock return empty array for listModels
+    when(modelDispatcher.listModels(modelNs)).thenReturn(new 
NameIdentifier[0]);
+    Response resp3 =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp3.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp3.getMediaType());
+
+    EntityListResponse resp4 = resp3.readEntity(EntityListResponse.class);
+    Assertions.assertEquals(0, resp4.getCode());
+    Assertions.assertEquals(0, resp4.identifiers().length);
+
+    // Test mock throw NoSuchSchemaException
+    doThrow(new NoSuchSchemaException("mock 
error")).when(modelDispatcher).listModels(modelNs);
+    Response resp5 =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp5.getStatus());
+
+    ErrorResponse errorResp = resp5.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResp.getCode());
+    Assertions.assertEquals(NoSuchSchemaException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(modelDispatcher).listModels(modelNs);
+    Response resp6 =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp6.getStatus());
+
+    ErrorResponse errorResp1 = resp6.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp1.getType());
+  }
+
+  @Test
+  public void testGetModel() {
+    Model mockModel = mockModel("model1", "comment1", 0);
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    when(modelDispatcher.getModel(modelId)).thenReturn(mockModel);
+
+    Response resp =
+        target(modelPath())
+            .path("model1")
+            .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());
+
+    ModelResponse modelResp = resp.readEntity(ModelResponse.class);
+    Assertions.assertEquals(0, modelResp.getCode());
+
+    Model resultModel = modelResp.getModel();
+    compare(mockModel, resultModel);
+
+    // Test mock throw NoSuchModelException
+    doThrow(new NoSuchModelException("mock 
error")).when(modelDispatcher).getModel(modelId);
+    Response resp1 =
+        target(modelPath())
+            .path("model1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    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(NoSuchModelException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(modelDispatcher).getModel(modelId);
+    Response resp2 =
+        target(modelPath())
+            .path("model1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    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 testRegisterModel() {
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    Model mockModel = mockModel("model1", "comment1", 0);
+    when(modelDispatcher.registerModel(modelId, "comment1", 
properties)).thenReturn(mockModel);
+
+    ModelRegisterRequest req = new ModelRegisterRequest("model1", "comment1", 
properties);
+    Response resp =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    ModelResponse modelResp = resp.readEntity(ModelResponse.class);
+    Assertions.assertEquals(0, modelResp.getCode());
+    compare(mockModel, modelResp.getModel());
+
+    // Test mock throw NoSuchSchemaException
+    doThrow(new NoSuchSchemaException("mock error"))
+        .when(modelDispatcher)
+        .registerModel(modelId, "comment1", properties);
+
+    Response resp1 =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, 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(NoSuchSchemaException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test mock throw ModelAlreadyExistsException
+    doThrow(new ModelAlreadyExistsException("mock error"))
+        .when(modelDispatcher)
+        .registerModel(modelId, "comment1", properties);
+
+    Response resp2 =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp2.getStatus());
+
+    ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(
+        ModelAlreadyExistsException.class.getSimpleName(), 
errorResp1.getType());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(modelDispatcher)
+        .registerModel(modelId, "comment1", properties);
+
+    Response resp3 =
+        target(modelPath())
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp3.getStatus());
+
+    ErrorResponse errorResp2 = resp3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp2.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp2.getType());
+  }
+
+  @Test
+  public void testDeleteModel() {
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    when(modelDispatcher.deleteModel(modelId)).thenReturn(true);
+
+    Response resp =
+        target(modelPath())
+            .path("model1")
+            .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());
+
+    // Test mock return false for deleteModel
+    when(modelDispatcher.deleteModel(modelId)).thenReturn(false);
+    Response resp1 =
+        target(modelPath())
+            .path("model1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp1.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp1.getMediaType());
+
+    DropResponse dropResp1 = resp1.readEntity(DropResponse.class);
+    Assertions.assertEquals(0, dropResp1.getCode());
+    Assertions.assertFalse(dropResp1.dropped());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(modelDispatcher).deleteModel(modelId);
+    Response resp2 =
+        target(modelPath())
+            .path("model1")
+            .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 testListModelVersions() {
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    int[] versions = new int[] {0, 1, 2};
+    when(modelDispatcher.listModelVersions(modelId)).thenReturn(versions);
+
+    Response resp =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .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());
+
+    ModelVersionListResponse versionListResp = 
resp.readEntity(ModelVersionListResponse.class);
+    Assertions.assertEquals(0, versionListResp.getCode());
+    Assertions.assertArrayEquals(versions, versionListResp.getVersions());
+
+    // Test mock return null for listModelVersions
+    when(modelDispatcher.listModelVersions(modelId)).thenReturn(null);
+    Response resp1 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp1.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp1.getMediaType());
+
+    ModelVersionListResponse versionListResp1 = 
resp1.readEntity(ModelVersionListResponse.class);
+    Assertions.assertEquals(0, versionListResp1.getCode());
+    Assertions.assertEquals(0, versionListResp1.getVersions().length);
+
+    // Test mock return empty array for listModelVersions
+    when(modelDispatcher.listModelVersions(modelId)).thenReturn(new int[0]);
+    Response resp2 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp2.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp2.getMediaType());
+
+    ModelVersionListResponse versionListResp2 = 
resp2.readEntity(ModelVersionListResponse.class);
+    Assertions.assertEquals(0, versionListResp2.getCode());
+    Assertions.assertEquals(0, versionListResp2.getVersions().length);
+
+    // Test mock throw NoSuchModelException
+    doThrow(new NoSuchModelException("mock error"))
+        .when(modelDispatcher)
+        .listModelVersions(modelId);
+    Response resp3 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .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(NoSuchModelException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock 
error")).when(modelDispatcher).listModelVersions(modelId);
+    Response resp4 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .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 testGetModelVersion() {
+    NameIdentifier modelIdent = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    ModelVersion mockModelVersion =
+        mockModelVersion(0, "uri1", new String[] {"alias1"}, "comment1");
+    when(modelDispatcher.getModelVersion(modelIdent, 
0)).thenReturn(mockModelVersion);
+
+    Response resp =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .path("0")
+            .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());
+
+    ModelVersionResponse versionResp = 
resp.readEntity(ModelVersionResponse.class);
+    Assertions.assertEquals(0, versionResp.getCode());
+    compare(mockModelVersion, versionResp.getModelVersion());
+
+    // Test mock throw NoSuchModelVersionException
+    doThrow(new NoSuchModelException("mock error"))
+        .when(modelDispatcher)
+        .getModelVersion(modelIdent, 0);
+
+    Response resp1 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .path("0")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    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(NoSuchModelException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(modelDispatcher)
+        .getModelVersion(modelIdent, 0);
+
+    Response resp2 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .path("0")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    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 get model version by alias
+    when(modelDispatcher.getModelVersion(modelIdent, 
"alias1")).thenReturn(mockModelVersion);
+
+    Response resp3 =
+        target(modelPath())
+            .path("model1")
+            .path("aliases")
+            .path("alias1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp3.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp3.getMediaType());
+
+    ModelVersionResponse versionResp1 = 
resp3.readEntity(ModelVersionResponse.class);
+    Assertions.assertEquals(0, versionResp1.getCode());
+    compare(mockModelVersion, versionResp1.getModelVersion());
+
+    // Test mock throw NoSuchModelVersionException
+    doThrow(new NoSuchModelException("mock error"))
+        .when(modelDispatcher)
+        .getModelVersion(modelIdent, "alias1");
+
+    Response resp4 =
+        target(modelPath())
+            .path("model1")
+            .path("aliases")
+            .path("alias1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp4.getStatus());
+
+    ErrorResponse errorResp2 = resp4.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResp2.getCode());
+    Assertions.assertEquals(NoSuchModelException.class.getSimpleName(), 
errorResp2.getType());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(modelDispatcher)
+        .getModelVersion(modelIdent, "alias1");
+
+    Response resp5 =
+        target(modelPath())
+            .path("model1")
+            .path("aliases")
+            .path("alias1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp5.getStatus());
+
+    ErrorResponse errorResp3 = resp5.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp3.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp3.getType());
+  }
+
+  @Test
+  public void testLinkModelVersion() {
+    NameIdentifier modelIdent = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    doNothing()
+        .when(modelDispatcher)
+        .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"}, 
"comment1", properties);
+
+    ModelVersionLinkRequest req =
+        new ModelVersionLinkRequest("uri1", new String[] {"alias1"}, 
"comment1", properties);
+
+    Response resp =
+        target(modelPath())
+            .path("model1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    BaseResponse baseResponse = resp.readEntity(BaseResponse.class);
+    Assertions.assertEquals(0, baseResponse.getCode());
+
+    // Test mock throw NoSuchModelException
+    doThrow(new NoSuchModelException("mock error"))
+        .when(modelDispatcher)
+        .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"}, 
"comment1", properties);
+
+    Response resp1 =
+        target(modelPath())
+            .path("model1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, 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(NoSuchModelException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test mock throw ModelVersionAliasesAlreadyExistException
+    doThrow(new ModelAlreadyExistsException("mock error"))
+        .when(modelDispatcher)
+        .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"}, 
"comment1", properties);
+
+    Response resp2 =
+        target(modelPath())
+            .path("model1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp2.getStatus());
+
+    ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE, 
errorResp1.getCode());
+    Assertions.assertEquals(
+        ModelAlreadyExistsException.class.getSimpleName(), 
errorResp1.getType());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(modelDispatcher)
+        .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"}, 
"comment1", properties);
+
+    Response resp3 =
+        target(modelPath())
+            .path("model1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp3.getStatus());
+
+    ErrorResponse errorResp2 = resp3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp2.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp2.getType());
+  }
+
+  @Test
+  public void testDeleteModelVersion() {
+    NameIdentifier modelIdent = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, "model1");
+    when(modelDispatcher.deleteModelVersion(modelIdent, 0)).thenReturn(true);
+
+    Response resp =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .path("0")
+            .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());
+
+    // Test mock return false for deleteModelVersion
+    when(modelDispatcher.deleteModelVersion(modelIdent, 0)).thenReturn(false);
+
+    Response resp1 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .path("0")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp1.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp1.getMediaType());
+
+    DropResponse dropResp1 = resp1.readEntity(DropResponse.class);
+    Assertions.assertEquals(0, dropResp1.getCode());
+    Assertions.assertFalse(dropResp1.dropped());
+
+    // Test mock return true for deleteModelVersion using alias
+    when(modelDispatcher.deleteModelVersion(modelIdent, 
"alias1")).thenReturn(true);
+
+    Response resp2 =
+        target(modelPath())
+            .path("model1")
+            .path("aliases")
+            .path("alias1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp2.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp2.getMediaType());
+
+    DropResponse dropResp2 = resp2.readEntity(DropResponse.class);
+    Assertions.assertEquals(0, dropResp2.getCode());
+
+    // Test mock return false for deleteModelVersion using alias
+    when(modelDispatcher.deleteModelVersion(modelIdent, 
"alias1")).thenReturn(false);
+
+    Response resp3 =
+        target(modelPath())
+            .path("model1")
+            .path("aliases")
+            .path("alias1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp3.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp3.getMediaType());
+
+    DropResponse dropResp3 = resp3.readEntity(DropResponse.class);
+    Assertions.assertEquals(0, dropResp3.getCode());
+    Assertions.assertFalse(dropResp3.dropped());
+
+    // Test mock throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(modelDispatcher)
+        .deleteModelVersion(modelIdent, 0);
+
+    Response resp4 =
+        target(modelPath())
+            .path("model1")
+            .path("versions")
+            .path("0")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    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 mock throw RuntimeException using alias
+    doThrow(new RuntimeException("mock error"))
+        .when(modelDispatcher)
+        .deleteModelVersion(modelIdent, "alias1");
+
+    Response resp5 =
+        target(modelPath())
+            .path("model1")
+            .path("aliases")
+            .path("alias1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .delete();
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp5.getStatus());
+
+    ErrorResponse errorResp2 = resp5.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp2.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp2.getType());
+  }
+
+  private String modelPath() {
+    return "/metalakes/" + metalake + "/catalogs/" + catalog + "/schemas/" + 
schema + "/models";
+  }
+
+  private Model mockModel(String modelName, String comment, int latestVersion) 
{
+    Model mockModel = mock(Model.class);
+    when(mockModel.name()).thenReturn(modelName);
+    when(mockModel.comment()).thenReturn(comment);
+    when(mockModel.latestVersion()).thenReturn(latestVersion);
+    when(mockModel.properties()).thenReturn(properties);
+    when(mockModel.auditInfo()).thenReturn(testAuditInfo);
+    return mockModel;
+  }
+
+  private ModelVersion mockModelVersion(int version, String uri, String[] 
aliases, String comment) {
+    ModelVersion mockModelVersion = mock(ModelVersion.class);
+    when(mockModelVersion.version()).thenReturn(version);
+    when(mockModelVersion.uri()).thenReturn(uri);
+    when(mockModelVersion.aliases()).thenReturn(aliases);
+    when(mockModelVersion.comment()).thenReturn(comment);
+    when(mockModelVersion.properties()).thenReturn(properties);
+    when(mockModelVersion.auditInfo()).thenReturn(testAuditInfo);
+    return mockModelVersion;
+  }
+
+  private void compare(Model left, Model right) {
+    Assertions.assertEquals(left.name(), right.name());
+    Assertions.assertEquals(left.comment(), right.comment());
+    Assertions.assertEquals(left.properties(), right.properties());
+
+    Assertions.assertNotNull(right.auditInfo());
+    Assertions.assertEquals(left.auditInfo().creator(), 
right.auditInfo().creator());
+    Assertions.assertEquals(left.auditInfo().createTime(), 
right.auditInfo().createTime());
+    Assertions.assertEquals(left.auditInfo().lastModifier(), 
right.auditInfo().lastModifier());
+    Assertions.assertEquals(
+        left.auditInfo().lastModifiedTime(), 
right.auditInfo().lastModifiedTime());
+  }
+
+  private void compare(ModelVersion left, ModelVersion right) {
+    Assertions.assertEquals(left.version(), right.version());
+    Assertions.assertEquals(left.uri(), right.uri());
+    Assertions.assertArrayEquals(left.aliases(), right.aliases());
+    Assertions.assertEquals(left.comment(), right.comment());
+    Assertions.assertEquals(left.properties(), right.properties());
+
+    Assertions.assertNotNull(right.auditInfo());
+    Assertions.assertEquals(left.auditInfo().creator(), 
right.auditInfo().creator());
+    Assertions.assertEquals(left.auditInfo().createTime(), 
right.auditInfo().createTime());
+    Assertions.assertEquals(left.auditInfo().lastModifier(), 
right.auditInfo().lastModifier());
+    Assertions.assertEquals(
+        left.auditInfo().lastModifiedTime(), 
right.auditInfo().lastModifiedTime());
+  }
+}

Reply via email to