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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 89bf021a32 [rest] Fix OpenAPI contract for client generation (#9289)
89bf021a32 is described below

commit 89bf021a326127207be2208ecd574d1aa38358f1
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Aug 18 21:48:21 2026 +0800

    [rest] Fix OpenAPI contract for client generation (#9289)
---
 docs/README.md                                     |   5 +-
 docs/docs/concepts/rest/rest-api.md                |   3 +
 docs/package.json                                  |   6 +-
 docs/scripts/validate-rest-openapi.js              | 200 +++++++++++++
 docs/static/rest-catalog-open-api.yaml             | 324 ++++++++++-----------
 .../org/apache/paimon/rest/RESTApiJsonTest.java    |  56 +++-
 6 files changed, 418 insertions(+), 176 deletions(-)

diff --git a/docs/README.md b/docs/README.md
index d05c37f3ba..67d65ae18e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -22,6 +22,9 @@ The site will be available at 
http://localhost:3000/docs/master/.
 ## Build
 
 ```bash
+# Validate the REST Catalog OpenAPI contract
+yarn test:rest-openapi
+
 # Production build
 yarn build
 
@@ -40,7 +43,7 @@ docs/
 │   ├── css/           # Custom styles
 │   └── plugins/       # Remark plugins (variable interpolation)
 ├── static/            # Static assets (images, logos, OpenAPI spec)
-├── scripts/           # Migration utilities
+├── scripts/           # Documentation validation and migration utilities
 ├── docusaurus.config.js
 ├── sidebars.js
 └── package.json
diff --git a/docs/docs/concepts/rest/rest-api.md 
b/docs/docs/concepts/rest/rest-api.md
index c22e3784cc..8a2f2e69e0 100644
--- a/docs/docs/concepts/rest/rest-api.md
+++ b/docs/docs/concepts/rest/rest-api.md
@@ -22,6 +22,9 @@ specific language governing permissions and limitations
 under the License.
 -->
 
+The OpenAPI 3.1 document below defines the language-neutral wire contract for 
REST Catalog
+servers and clients. It can also be used to generate or validate SDK models in 
other languages.
+
 <body>
     <iframe src="/docs/master/rest-catalog-open-api.yaml" width="100%" 
height="800px" />
 </body>
diff --git a/docs/package.json b/docs/package.json
index 39727101ac..f0fb43967b 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -5,7 +5,8 @@
   "scripts": {
     "docusaurus": "docusaurus",
     "start": "docusaurus start",
-    "build": "docusaurus build",
+    "build": "yarn test:rest-openapi && docusaurus build",
+    "test:rest-openapi": "node scripts/validate-rest-openapi.js",
     "swizzle": "docusaurus swizzle",
     "deploy": "docusaurus deploy",
     "clear": "docusaurus clear",
@@ -25,7 +26,8 @@
   },
   "devDependencies": {
     "@docusaurus/module-type-aliases": "^3.7.0",
-    "@docusaurus/types": "^3.7.0"
+    "@docusaurus/types": "^3.7.0",
+    "js-yaml": "^4.1.0"
   },
   "browserslist": {
     "production": [
diff --git a/docs/scripts/validate-rest-openapi.js 
b/docs/scripts/validate-rest-openapi.js
new file mode 100644
index 0000000000..62e3598cf5
--- /dev/null
+++ b/docs/scripts/validate-rest-openapi.js
@@ -0,0 +1,200 @@
+/*
+ * 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.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const yaml = require('js-yaml');
+
+const specPath = path.resolve(__dirname, '..', 'static', 
'rest-catalog-open-api.yaml');
+const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
+
+function check(condition, message) {
+  if (!condition) {
+    throw new Error(message);
+  }
+}
+
+function decodePointerSegment(segment) {
+  return segment.replace(/~1/g, '/').replace(/~0/g, '~');
+}
+
+function resolveLocalRef(ref) {
+  check(ref.startsWith('#/'), `Only local OpenAPI references are supported, 
found: ${ref}`);
+  return ref
+    .slice(2)
+    .split('/')
+    .map(decodePointerSegment)
+    .reduce((current, segment) => {
+      check(
+        current && Object.prototype.hasOwnProperty.call(current, segment),
+        `Unresolved OpenAPI reference: ${ref}`,
+      );
+      return current[segment];
+    }, spec);
+}
+
+function visit(value) {
+  if (Array.isArray(value)) {
+    value.forEach(visit);
+    return;
+  }
+  if (!value || typeof value !== 'object') {
+    return;
+  }
+  check(!Object.prototype.hasOwnProperty.call(value, 'nullable'), 'OpenAPI 3.1 
schemas must not use nullable');
+  if (typeof value.$ref === 'string') {
+    resolveLocalRef(value.$ref);
+  }
+  Object.values(value).forEach(visit);
+}
+
+function schema(name) {
+  const value = spec.components && spec.components.schemas && 
spec.components.schemas[name];
+  check(value, `Missing OpenAPI schema: ${name}`);
+  return value;
+}
+
+function requireProperties(schemaName, names) {
+  const properties = schema(schemaName).properties || {};
+  names.forEach((name) => {
+    check(properties[name], `Schema ${schemaName} is missing property: 
${name}`);
+  });
+  return properties;
+}
+
+function requireTypedIntegerProperties(schemaName, names) {
+  const properties = requireProperties(schemaName, names);
+  names.forEach((name) => {
+    check(
+      properties[name].type === 'integer' && properties[name].format === 
'int64',
+      `Schema ${schemaName}.${name} must be an int64 integer`,
+    );
+  });
+}
+
+function requireSchemaReference(schemaName, composition, referencedSchemaName) 
{
+  const references = schema(schemaName)[composition] || [];
+  const expected = `#/components/schemas/${referencedSchemaName}`;
+  check(
+    references.some((reference) => reference.$ref === expected),
+    `Schema ${schemaName}.${composition} is missing reference: ${expected}`,
+  );
+}
+
+function requireArrayOfIdentifiers(schemaName, propertyName) {
+  const properties = requireProperties(schemaName, [propertyName]);
+  check(properties[propertyName].type === 'array', `Schema 
${schemaName}.${propertyName} must be an array`);
+  check(
+    properties[propertyName].items &&
+      properties[propertyName].items.$ref === 
'#/components/schemas/Identifier',
+    `Schema ${schemaName}.${propertyName} items must reference Identifier`,
+  );
+}
+
+function requireNullableStringProperty(schemaName, propertyName) {
+  const property = requireProperties(schemaName, [propertyName])[propertyName];
+  check(
+    Array.isArray(property.type) &&
+      property.type.includes('string') &&
+      property.type.includes('null'),
+    `Schema ${schemaName}.${propertyName} must accept string and null`,
+  );
+}
+
+check(spec.openapi === '3.1.1', `Expected OpenAPI 3.1.1, found: 
${spec.openapi}`);
+check(spec.paths && spec.components && spec.components.schemas, 'Incomplete 
OpenAPI document');
+visit(spec);
+
+const operationIds = new Set();
+for (const pathItem of Object.values(spec.paths)) {
+  for (const operation of Object.values(pathItem)) {
+    if (!operation || typeof operation !== 'object' || !operation.operationId) 
{
+      continue;
+    }
+    check(!operationIds.has(operation.operationId), `Duplicate operationId: 
${operation.operationId}`);
+    operationIds.add(operation.operationId);
+  }
+}
+
+[
+  'getConfig',
+  'createDatabase',
+  'getDatabase',
+  'alterDatabase',
+  'dropDatabase',
+  'createTable',
+  'getTable',
+  'alterTable',
+  'dropTable',
+].forEach((operationId) => {
+  check(operationIds.has(operationId), `Missing provider-facing operationId: 
${operationId}`);
+});
+
+requireProperties('ConfigResponse', ['defaults', 'overrides']);
+requireProperties('CreateDatabaseRequest', ['name', 'options']);
+requireProperties('AlterDatabaseRequest', ['removals', 'updates']);
+requireProperties('CreateTableRequest', ['identifier', 'schema']);
+requireProperties('AlterTableRequest', ['changes']);
+requireProperties('Identifier', ['database', 'object']);
+requireProperties('Schema', ['fields', 'partitionKeys', 'primaryKeys', 
'options', 'comment']);
+requireProperties('DataField', ['id', 'name', 'type', 'description', 
'defaultValue']);
+
+requireSchemaReference('DataType', 'oneOf', 'VectorType');
+requireProperties('VectorType', ['type', 'element', 'length']);
+requireSchemaReference('SchemaChange', 'anyOf', 'DropPrimaryKey');
+check(
+  schema('BaseSchemaChange').discriminator.mapping.dropPrimaryKey ===
+    '#/components/schemas/DropPrimaryKey',
+  'BaseSchemaChange discriminator is missing dropPrimaryKey',
+);
+const dropPrimaryKey = requireProperties('DropPrimaryKey', ['action']);
+check(
+  dropPrimaryKey.action.const === 'dropPrimaryKey',
+  'Schema DropPrimaryKey.action must be dropPrimaryKey',
+);
+check(
+  schema('BaseInstant').discriminator.propertyName === 'type',
+  'BaseInstant discriminator must use the JSON field type',
+);
+
+const updateViewComment = requireProperties('UpdateViewComment', ['action', 
'comment']);
+check(!updateViewComment.key, 'Schema UpdateViewComment must use comment 
instead of key');
+['UpdateComment', 'UpdateViewComment', 
'UpdateFunctionComment'].forEach((schemaName) =>
+  requireNullableStringProperty(schemaName, 'comment'),
+);
+
+const errorResourceTypes = requireProperties('ErrorResponse', 
['resourceType']).resourceType.enum || [];
+['FUNCTION', 'DEFINITION'].forEach((resourceType) => {
+  check(
+    errorResourceTypes.includes(resourceType),
+    `Schema ErrorResponse.resourceType is missing value: ${resourceType}`,
+  );
+});
+
+requireArrayOfIdentifiers('ListTablesGloballyResponse', 'tables');
+requireArrayOfIdentifiers('ListViewsGloballyResponse', 'views');
+requireArrayOfIdentifiers('ListFunctionsGloballyResponse', 'functions');
+requireProperties('ListFunctionsGloballyResponse', ['nextPageToken']);
+const getFunctionProperties = requireProperties('GetFunctionResponse', 
['uuid']);
+check(getFunctionProperties.uuid.type === 'string', 'Schema 
GetFunctionResponse.uuid must be a string');
+
+['GetDatabaseResponse', 'GetTableResponse', 'GetViewResponse', 
'GetFunctionResponse'].forEach(
+  (schemaName) => requireTypedIntegerProperties(schemaName, ['createdAt', 
'updatedAt']),
+);
+
+console.log(`Validated REST OpenAPI contract with ${operationIds.size} 
operations.`);
diff --git a/docs/static/rest-catalog-open-api.yaml 
b/docs/static/rest-catalog-open-api.yaml
index 47f5acef5e..452919f53f 100644
--- a/docs/static/rest-catalog-open-api.yaml
+++ b/docs/static/rest-catalog-open-api.yaml
@@ -48,14 +48,11 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ConfigResponse'
-              example: {
-                "overrides": {
-                  "key": "value"
-                },
-                "defaults": {
-                  "prefix": "prefix"
-                }
-              }
+              example:
+                overrides:
+                  key: value
+                defaults:
+                  prefix: prefix
         "401":
           $ref: '#/components/responses/UnauthorizedErrorResponse'
         "500":
@@ -2229,10 +2226,9 @@ components:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
-          example: {
-            "message": "Malformed request",
-            "code": 400
-          }
+          example:
+            message: Malformed request
+            code: 400
     UnauthorizedErrorResponse:
       description:
         Used for 401 errors.
@@ -2240,10 +2236,9 @@ components:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
-          example: {
-            "message": "No auth for this resource",
-            "code": 401
-          }
+          example:
+            message: No auth for this resource
+            code: 401
     ForbiddenErrorResponse:
       description:
         Used for 403 errors.
@@ -2251,10 +2246,9 @@ components:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
-          example: {
-            "message": "Table has no permission",
-            "code": 403
-          }
+          example:
+            message: Table has no permission
+            code: 403
     ResourceNotExistErrorResponse:
       description:
         Used for 404 errors, which means the resource does not exist.
@@ -2262,12 +2256,11 @@ components:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
-          example: {
-            "message": "Resource is not exist",
-            "resourceType": "TABLE",
-            "resourceName": "user",
-            "code": 404
-          }
+          example:
+            message: Resource does not exist
+            resourceType: TABLE
+            resourceName: user
+            code: 404
     DatabaseNotExistErrorResponse:
       description:
         Not Found - DatabaseNotExistException, the database does not exist
@@ -2276,12 +2269,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given database does not exist",
-              "resourceType": "DATABASE",
-              "resourceName": "db",
-              "code": 404
-            }
+            message: The given database does not exist
+            resourceType: DATABASE
+            resourceName: db
+            code: 404
     TableNotExistErrorResponse:
       description:
         Not Found - TableNotExistException, the table does not exist
@@ -2290,12 +2281,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given table does not exist",
-              "resourceType": "TABLE",
-              "resourceName": "table",
-              "code": 404
-            }
+            message: The given table does not exist
+            resourceType: TABLE
+            resourceName: table
+            code: 404
     SnapshotNotExistErrorResponse:
       description:
         Not Found - SnapshotNotExistException, the snapshot does not exist
@@ -2304,12 +2293,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given snapshot does not exist",
-              "resourceType": "SNAPSHOT",
-              "resourceName": "1",
-              "code": 404
-            }
+            message: The given snapshot does not exist
+            resourceType: SNAPSHOT
+            resourceName: "1"
+            code: 404
     BranchNotExistErrorResponse:
       description:
         Not Found - BranchNotExistException, the branch does not exist
@@ -2318,12 +2305,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given branch does not exist",
-              "resourceType": "BRANCH",
-              "resourceName": "branch",
-              "code": 404
-            }
+            message: The given branch does not exist
+            resourceType: BRANCH
+            resourceName: branch
+            code: 404
     TagNotExistErrorResponse:
       description:
         Not Found - TagNotExistException, the tag does not exist
@@ -2332,12 +2317,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given tag does not exist",
-              "resourceType": "TAG",
-              "resourceName": "tag1",
-              "code": 404
-            }
+            message: The given tag does not exist
+            resourceType: TAG
+            resourceName: tag1
+            code: 404
     ViewNotExistErrorResponse:
       description:
         Not Found - ViewNotExistException, the view does not exist
@@ -2346,12 +2329,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given view does not exist",
-              "resourceType": "VIEW",
-              "resourceName": "view",
-              "code": 404
-            }
+            message: The given view does not exist
+            resourceType: VIEW
+            resourceName: view
+            code: 404
     FunctionNotExistErrorResponse:
       description:
         Not Found - FunctionNotExistException, the function does not exist
@@ -2360,12 +2341,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given function does not exist",
-              "resourceType": "FUNCTION",
-              "resourceName": "function",
-              "code": 404
-            }
+            message: The given function does not exist
+            resourceType: FUNCTION
+            resourceName: function
+            code: 404
     ResourceAlreadyExistErrorResponse:
       description:
         Used for 409 errors.
@@ -2373,12 +2352,11 @@ components:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
-          example: {
-            "message": "Resource has exist",
-            "resourceType": "TABLE",
-            "resourceName": "account",
-            "code": 409
-          }
+          example:
+            message: Resource already exists
+            resourceType: TABLE
+            resourceName: account
+            code: 409
     DatabaseAlreadyExistErrorResponse:
       description: Conflict - The database already exists
       content:
@@ -2386,12 +2364,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given database already exists",
-              "resourceType": "DATABASE",
-              "resourceName": "db",
-              "code": 409
-            }
+            message: The given database already exists
+            resourceType: DATABASE
+            resourceName: db
+            code: 409
     TableAlreadyExistErrorResponse:
       description: Conflict - The table already exists
       content:
@@ -2399,12 +2375,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given table already exists",
-              "resourceType": "TABLE",
-              "resourceName": "table",
-              "code": 409
-            }
+            message: The given table already exists
+            resourceType: TABLE
+            resourceName: table
+            code: 409
     BranchAlreadyExistErrorResponse:
       description: Conflict - The branch already exists
       content:
@@ -2412,12 +2386,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given branch already exists",
-              "resourceType": "BRANCH",
-              "resourceName": "branch",
-              "code": 409
-            }
+            message: The given branch already exists
+            resourceType: BRANCH
+            resourceName: branch
+            code: 409
     TagAlreadyExistErrorResponse:
       description: Conflict - The tag already exists
       content:
@@ -2425,12 +2397,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given tag already exists",
-              "resourceType": "TAG",
-              "resourceName": "tag1",
-              "code": 409
-            }
+            message: The given tag already exists
+            resourceType: TAG
+            resourceName: tag1
+            code: 409
     ViewAlreadyExistErrorResponse:
       description: Conflict - The view already exists
       content:
@@ -2438,12 +2408,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given view already exists",
-              "resourceType": "VIEW",
-              "resourceName": "view",
-              "code": 409
-            }
+            message: The given view already exists
+            resourceType: VIEW
+            resourceName: view
+            code: 409
     FunctionAlreadyExistErrorResponse:
       description: Conflict - The view already exists
       content:
@@ -2451,12 +2419,10 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
           example:
-            {
-              "message": "The given function already exists",
-              "resourceType": "FUNCTION",
-              "resourceName": "function",
-              "code": 409
-            }
+            message: The given function already exists
+            resourceType: FUNCTION
+            resourceName: function
+            code: 409
     ServerErrorResponse:
       description:
         Used for server 5xx errors.
@@ -2464,10 +2430,9 @@ components:
         application/json:
           schema:
             $ref: '#/components/schemas/ErrorResponse'
-          example: {
-            "message": "Internal Server Error",
-            "code": 500
-          }
+          example:
+            message: Internal Server Error
+            code: 500
   schemas:
     CreateDatabaseRequest:
       type: object
@@ -2599,12 +2564,10 @@ components:
         message:
           type: string
         resourceType:
-          type: string
-          nullable: true
-          enum: [ "DATABASE", "TABLE", "PARTITION", "COLUMN", "SNAPSHOT", 
"BRANCH", "TAG", "VIEW", "DIALECT", "UNKNOWN" ]
+          type: [ string, "null" ]
+          enum: [ "DATABASE", "TABLE", "PARTITION", "COLUMN", "SNAPSHOT", 
"BRANCH", "TAG", "VIEW", "DIALECT", "FUNCTION", "DEFINITION", "UNKNOWN", null ]
         resourceName:
-          type: string
-          nullable: true
+          type: [ string, "null" ]
         code:
           type: integer
           format: int32
@@ -2720,7 +2683,7 @@ components:
           type: string
           const: "updateComment"
         comment:
-          type: string
+          type: [ string, "null" ]
     AddDefinition:
       allOf:
         - $ref: '#/components/schemas/BaseFunctionChange'
@@ -2865,8 +2828,8 @@ components:
         action:
           type: string
           const: "updateComment"
-        key:
-          type: string
+        comment:
+          type: [ string, "null" ]
     AddDialect:
       allOf:
         - $ref: '#/components/schemas/BaseViewChange'
@@ -2910,6 +2873,8 @@ components:
           $ref: '#/components/schemas/DataType'
         description:
           type: string
+        defaultValue:
+          type: string
     DataType:
       oneOf:
         - $ref: '#/components/schemas/PrimitiveType'
@@ -2917,6 +2882,7 @@ components:
         - $ref: '#/components/schemas/MultisetType'
         - $ref: '#/components/schemas/MapType'
         - $ref: '#/components/schemas/RowType'
+        - $ref: '#/components/schemas/VectorType'
     PrimitiveType:
       type: string
     ArrayType:
@@ -2959,6 +2925,18 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/DataField'
+    VectorType:
+      type: object
+      properties:
+        type:
+          type: string
+          pattern: ^VECTOR.*
+          example: VECTOR
+        element:
+          $ref: '#/components/schemas/DataType'
+        length:
+          type: integer
+          format: int32
     Identifier:
       type: object
       properties:
@@ -3008,10 +2986,12 @@ components:
         owner:
           type: string
         createdAt:
+          type: integer
           format: int64
         createdBy:
           type: string
         updatedAt:
+          type: integer
           format: int64
         updatedBy:
           type: string
@@ -3028,6 +3008,7 @@ components:
         - $ref: '#/components/schemas/UpdateColumnType'
         - $ref: '#/components/schemas/UpdateColumnPosition'
         - $ref: '#/components/schemas/UpdateColumnNullability'
+        - $ref: '#/components/schemas/DropPrimaryKey'
     BaseSchemaChange:
       discriminator:
         propertyName: action
@@ -3043,6 +3024,7 @@ components:
           updateColumnType: '#/components/schemas/UpdateColumnType'
           updateColumnPosition: '#/components/schemas/UpdateColumnPosition'
           updateColumnNullability: 
'#/components/schemas/UpdateColumnNullability'
+          dropPrimaryKey: '#/components/schemas/DropPrimaryKey'
       type: object
       required:
         - action
@@ -3077,7 +3059,7 @@ components:
           type: string
           const: "updateComment"
         comment:
-          type: string
+          type: [ string, "null" ]
     AddColumn:
       allOf:
         - $ref: '#/components/schemas/BaseSchemaChange'
@@ -3182,6 +3164,13 @@ components:
             type: string
         newNullability:
           type: boolean
+    DropPrimaryKey:
+      allOf:
+        - $ref: '#/components/schemas/BaseSchemaChange'
+      properties:
+        action:
+          type: string
+          const: "dropPrimaryKey"
     Move:
       type: object
       properties:
@@ -3204,8 +3193,7 @@ components:
         tableId:
           type: string
         baseSnapshotUuid:
-          type: string
-          nullable: true
+          type: [ string, "null" ]
         snapshot:
           $ref: '#/components/schemas/Snapshot'
         statistics:
@@ -3218,9 +3206,8 @@ components:
         instant:
           $ref: '#/components/schemas/Instant'
         fromSnapshot:
-          type: integer
+          type: [ integer, "null" ]
           format: int64
-          nullable: true
     RollbackSchemaRequest:
       type: object
       required:
@@ -3235,7 +3222,7 @@ components:
         - $ref: '#/components/schemas/TagInstant'
     BaseInstant:
       discriminator:
-        propertyName: action
+        propertyName: type
         mapping:
           snapshot: '#/components/schemas/SnapshotInstant'
           tag: '#/components/schemas/TagInstant'
@@ -3268,12 +3255,10 @@ components:
       type: object
       properties:
         version:
-          type: integer
+          type: [ integer, "null" ]
           format: int32
-          nullable: true
         uuid:
-          type: string
-          nullable: true
+          type: [ string, "null" ]
         id:
           type: integer
           format: int64
@@ -3285,8 +3270,7 @@ components:
         deltaManifestList:
           type: string
         changelogManifestList:
-          type: string
-          nullable: true
+          type: [ string, "null" ]
         indexManifest:
           type: string
         commitUser:
@@ -3439,10 +3423,12 @@ components:
         owner:
           type: string
         createdAt:
+          type: integer
           format: int64
         createdBy:
           type: string
         updatedAt:
+          type: integer
           format: int64
         updatedBy:
           type: string
@@ -3471,8 +3457,7 @@ components:
         tables:
           type: array
           items:
-            identifier:
-              $ref: '#/components/schemas/Identifier'
+            $ref: '#/components/schemas/Identifier'
         nextPageToken:
           type: string
     ConfigResponse:
@@ -3501,8 +3486,7 @@ components:
         branch:
           type: string
         fromTag:
-          nullable: true
-          type: string
+          type: [ string, "null" ]
     RenameBranchRequest:
       type: object
       properties:
@@ -3528,13 +3512,11 @@ components:
         tagName:
           type: string
         snapshotId:
-          type: integer
+          type: [ integer, "null" ]
           format: int64
-          nullable: true
           description: Optional snapshot id, if not provided uses latest 
snapshot
         timeRetained:
-          type: string
-          nullable: true
+          type: [ string, "null" ]
           description: Optional time retained as string (e.g., "1d", "12h", 
"30m")
         ignoreIfExists:
           type: boolean
@@ -3548,12 +3530,10 @@ components:
         snapshot:
           $ref: '#/components/schemas/Snapshot'
         tagCreateTime:
-          type: integer
+          type: [ integer, "null" ]
           format: int64
-          nullable: true
         tagTimeRetained:
-          type: string
-          nullable: true
+          type: [ string, "null" ]
     ListTagsResponse:
       type: object
       properties:
@@ -3572,12 +3552,10 @@ components:
           type: integer
           format: int64
         tagCreateTime:
-          type: integer
+          type: [ integer, "null" ]
           format: int64
-          nullable: true
         tagTimeRetained:
-          type: string
-          nullable: true
+          type: [ string, "null" ]
     GetViewResponse:
       type: object
       properties:
@@ -3590,10 +3568,12 @@ components:
         owner:
           type: string
         createdAt:
+          type: integer
           format: int64
         createdBy:
           type: string
         updatedAt:
+          type: integer
           format: int64
         updatedBy:
           type: string
@@ -3623,14 +3603,15 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/Identifier'
+        nextPageToken:
+          type: string
     ListViewsGloballyResponse:
       type: object
       properties:
-        tables:
+        views:
           type: array
           items:
-            identifier:
-              $ref: '#/components/schemas/Identifier'
+            $ref: '#/components/schemas/Identifier'
         nextPageToken:
           type: string
     ListFunctionsResponse:
@@ -3654,6 +3635,8 @@ components:
     GetFunctionResponse:
       type: object
       properties:
+        uuid:
+          type: string
         name:
           type: string
         inputParams:
@@ -3679,10 +3662,12 @@ components:
         owner:
           type: string
         createdAt:
+          type: integer
           format: int64
         createdBy:
           type: string
         updatedAt:
+          type: integer
           format: int64
         updatedBy:
           type: string
@@ -3799,28 +3784,25 @@ components:
   examples:
     TableNotExistError:
       summary: The requested table does not exist
-      value: {
-        "message": "The given table does not exist",
-        "resourceType": "TABLE",
-        "resourceName": "table",
-        "code": 404
-      }
+      value:
+        message: The given table does not exist
+        resourceType: TABLE
+        resourceName: table
+        code: 404
     SnapshotNotExistError:
       summary: The requested snapshot does not exist
-      value: {
-        "message": "The given snapshot does not exist",
-        "resourceType": "SNAPSHOT",
-        "resourceName": "1",
-        "code": 404
-      }
+      value:
+        message: The given snapshot does not exist
+        resourceType: SNAPSHOT
+        resourceName: "1"
+        code: 404
     TagNotExistError:
       summary: The requested tag does not exist
-      value: {
-        "message": "The given tag does not exist",
-        "resourceType": "TAG",
-        "resourceName": "tag1",
-        "code": 404
-      }
+      value:
+        message: The given tag does not exist
+        resourceType: TAG
+        resourceName: tag1
+        code: 404
   securitySchemes:
     BearerAuth:
       type: http
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
index 7508e00473..4a8b4ed964 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.rest;
 
+import org.apache.paimon.function.FunctionChange;
 import org.apache.paimon.rest.requests.AlterDatabaseRequest;
 import org.apache.paimon.rest.requests.AlterFunctionRequest;
 import org.apache.paimon.rest.requests.AlterTableRequest;
@@ -46,10 +47,12 @@ import 
org.apache.paimon.rest.responses.ListDatabasesResponse;
 import org.apache.paimon.rest.responses.ListPartitionsResponse;
 import org.apache.paimon.rest.responses.ListTablesResponse;
 import org.apache.paimon.rest.responses.ListViewsResponse;
+import org.apache.paimon.schema.SchemaChange;
 import org.apache.paimon.table.Instant;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.IntType;
+import org.apache.paimon.view.ViewChange;
 
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
 
@@ -155,15 +158,61 @@ public class RESTApiJsonTest {
         String name = "col1";
         IntType type = DataTypes.INT();
         String descStr = "desc";
+        String defaultValue = "42";
         String dataFieldStr =
                 String.format(
-                        "{\"id\": %d,\"name\":\"%s\",\"type\":\"%s\", 
\"description\":\"%s\"}",
-                        id, name, type, descStr);
+                        "{\"id\": %d,\"name\":\"%s\",\"type\":\"%s\","
+                                + 
"\"description\":\"%s\",\"defaultValue\":\"%s\"}",
+                        id, name, type, descStr, defaultValue);
         DataField parseData = RESTApi.fromJson(dataFieldStr, DataField.class);
         assertEquals(id, parseData.id());
         assertEquals(name, parseData.name());
         assertEquals(type, parseData.type());
         assertEquals(descStr, parseData.description());
+        assertEquals(defaultValue, parseData.defaultValue());
+
+        DataField field = new DataField(id, name, type, descStr, defaultValue);
+        assertEquals(field, RESTApi.fromJson(RESTApi.toJson(field), 
DataField.class));
+    }
+
+    @Test
+    public void providerFacingTypeAndChangeJsonShapeTest() throws Exception {
+        DataField vectorField =
+                new DataField(2, "embedding", DataTypes.VECTOR(3, 
DataTypes.FLOAT()));
+        Map<?, ?> vectorJson = RESTApi.fromJson(RESTApi.toJson(vectorField), 
Map.class);
+        Map<?, ?> vectorType = (Map<?, ?>) vectorJson.get("type");
+        assertEquals("VECTOR", vectorType.get("type"));
+        assertEquals("FLOAT", vectorType.get("element"));
+        assertEquals(3, vectorType.get("length"));
+
+        assertEquals(
+                Collections.singletonMap("action", "dropPrimaryKey"),
+                
RESTApi.fromJson(RESTApi.toJson(SchemaChange.dropPrimaryKey()), Map.class));
+
+        Map<String, Object> expectedViewComment = new HashMap<>();
+        expectedViewComment.put("action", "updateComment");
+        expectedViewComment.put("comment", "new comment");
+        assertEquals(
+                expectedViewComment,
+                RESTApi.fromJson(
+                        RESTApi.toJson(ViewChange.updateComment("new 
comment")), Map.class));
+    }
+
+    @Test
+    public void nullableCommentChangesJsonShapeTest() throws Exception {
+        Map<String, Object> expected = new HashMap<>();
+        expected.put("action", "updateComment");
+        expected.put("comment", null);
+
+        assertEquals(
+                expected,
+                
RESTApi.fromJson(RESTApi.toJson(SchemaChange.updateComment(null)), Map.class));
+        assertEquals(
+                expected,
+                
RESTApi.fromJson(RESTApi.toJson(ViewChange.updateComment(null)), Map.class));
+        assertEquals(
+                expected,
+                
RESTApi.fromJson(RESTApi.toJson(FunctionChange.updateComment(null)), 
Map.class));
     }
 
     @Test
@@ -363,6 +412,9 @@ public class RESTApiJsonTest {
         RollbackTableRequest rollbackTableRequestBySnapshot =
                 MockRESTMessage.rollbackTableRequestBySnapshot(snapshotId);
         String rollbackTableRequestBySnapshotStr = 
RESTApi.toJson(rollbackTableRequestBySnapshot);
+        Map<?, ?> rollbackJson = 
RESTApi.fromJson(rollbackTableRequestBySnapshotStr, Map.class);
+        Map<?, ?> instantJson = (Map<?, ?>) rollbackJson.get("instant");
+        assertEquals("snapshot", instantJson.get("type"));
         Instant.SnapshotInstant rollbackTableRequestParseData =
                 (Instant.SnapshotInstant)
                         RESTApi.fromJson(

Reply via email to