You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/06 14:27:51 UTC

(hugegraph) 03/14: fix(server): adapt GraphSON and Cypher

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

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

commit 421fbc6e547a5d286eb851bb2768a4d138f030e6
Author: contrueCT <co...@outlook.com>
AuthorDate: Tue Oct 6 01:17:45 2026 +0800

    fix(server): adapt GraphSON and Cypher
    
    - Adapt Cypher client and processor integration to the upgraded Driver APIs
    - Update GraphSON modules and type serializer registration
    - Retain Cypher and JSON serialization compatibility checks
---
 .../apache/hugegraph/api/cypher/CypherClient.java  | 102 +++++++++++++-
 .../hugegraph/opencypher/CypherOpProcessor.java    |  12 +-
 .../apache/hugegraph/io/HugeGraphSONModule.java    |  73 +++++++++-
 .../io/HugeGraphTypeSerializerRegistryBuilder.java |  72 ++++++++++
 .../java/org/apache/hugegraph/util/JsonUtil.java   |   1 +
 .../org/apache/hugegraph/api/CypherApiTest.java    | 149 ++++++++++++++++++++-
 .../hugegraph/api/cypher/CypherClientTest.java     | 129 ++++++++++++++++++
 .../unit/serializer/HugeGraphSONModuleTest.java    | 148 ++++++++++++++++++++
 .../apache/hugegraph/unit/util/JsonUtilTest.java   |  29 ++++
 9 files changed, 696 insertions(+), 19 deletions(-)

diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
index 92ae18c54..85d98a25c 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
@@ -17,7 +17,10 @@
 
 package org.apache.hugegraph.api.cypher;
 
+import java.lang.reflect.Array;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -29,20 +32,23 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
 import org.apache.commons.configuration2.Configuration;
+import org.apache.hugegraph.backend.id.Id;
 import org.apache.hugegraph.util.E;
 import org.apache.hugegraph.util.Log;
 import org.apache.tinkerpop.gremlin.driver.Client;
 import org.apache.tinkerpop.gremlin.driver.Cluster;
 import org.apache.tinkerpop.gremlin.driver.Result;
 import org.apache.tinkerpop.gremlin.driver.ResultSet;
-import org.apache.tinkerpop.gremlin.driver.Tokens;
-import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.util.Tokens;
+import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
 import org.slf4j.Logger;
 
 @ThreadSafe
 public final class CypherClient {
 
     private static final Logger LOG = Log.logger(CypherClient.class);
+    private static final int NORMALIZE_MAX_DEPTH = 32;
     private final Supplier<Configuration> configurationSupplier;
     private String userName;
     private String password;
@@ -105,12 +111,102 @@ public final class CypherClient {
 
         while (iter.hasNext()) {
             Result data = iter.next();
-            list.add(data.getObject());
+            list.add(normalize(data.getObject()));
         }
 
         return list;
     }
 
+    static Object normalize(Object value) {
+        return normalize(value, 0, new IdentityHashMap<>());
+    }
+
+    private static Object normalize(Object value, int depth,
+                                    IdentityHashMap<Object, Boolean> seen) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Id) {
+            return ((Id) value).asObject();
+        }
+        boolean composite = value instanceof Map || value instanceof Path ||
+                            value instanceof Iterable ||
+                            value.getClass().isArray();
+        if (!composite) {
+            return value;
+        }
+        if (depth >= NORMALIZE_MAX_DEPTH) {
+            throw new IllegalArgumentException(
+                      "Exceeded max normalization depth 32");
+        }
+        if (value instanceof Map) {
+            if (seen.put(value, Boolean.TRUE) != null) {
+                throw new IllegalArgumentException(
+                          "Detected cyclic Cypher result");
+            }
+            Map<Object, Object> normalized = new LinkedHashMap<>();
+            try {
+                for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
+                    normalized.put(normalize(entry.getKey(), depth + 1, seen),
+                                   normalize(entry.getValue(), depth + 1, seen));
+                }
+            } finally {
+                seen.remove(value);
+            }
+            return normalized;
+        }
+        if (value instanceof Path) {
+            if (seen.put(value, Boolean.TRUE) != null) {
+                throw new IllegalArgumentException(
+                          "Detected cyclic Cypher result");
+            }
+            Map<String, Object> normalized = new LinkedHashMap<>();
+            try {
+                Path path = (Path) value;
+                normalized.put("labels",
+                               normalize(path.labels(), depth + 1, seen));
+                normalized.put("objects",
+                               normalize(path.objects(), depth + 1, seen));
+            } finally {
+                seen.remove(value);
+            }
+            return normalized;
+        }
+        if (value instanceof Iterable) {
+            if (seen.put(value, Boolean.TRUE) != null) {
+                throw new IllegalArgumentException(
+                          "Detected cyclic Cypher result");
+            }
+            List<Object> normalized = new LinkedList<>();
+            try {
+                for (Object item : (Iterable<?>) value) {
+                    normalized.add(normalize(item, depth + 1, seen));
+                }
+            } finally {
+                seen.remove(value);
+            }
+            return normalized;
+        }
+        if (value.getClass().isArray()) {
+            if (seen.put(value, Boolean.TRUE) != null) {
+                throw new IllegalArgumentException(
+                          "Detected cyclic Cypher result");
+            }
+            List<Object> normalized = new LinkedList<>();
+            try {
+                int length = Array.getLength(value);
+                for (int i = 0; i < length; i++) {
+                    normalized.add(normalize(Array.get(value, i), depth + 1,
+                                             seen));
+                }
+            } finally {
+                seen.remove(value);
+            }
+            return normalized;
+        }
+        return value;
+    }
+
     /**
      * As Sasl does not support a token, which is a coded string to indicate a legal user,
      * we had to use a trick to fix it. When the token is set, the password will be set to
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
index dfad9b959..e3f0a6036 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
@@ -19,7 +19,7 @@ package org.apache.hugegraph.opencypher;
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static java.util.Optional.empty;
-import static org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode.SERVER_ERROR;
+import static org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode.SERVER_ERROR;
 import static org.opencypher.gremlin.translation.StatementOption.EXPLAIN;
 import static org.slf4j.LoggerFactory.getLogger;
 
@@ -33,10 +33,6 @@ import java.util.concurrent.Future;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
 
-import org.apache.tinkerpop.gremlin.driver.Tokens;
-import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal;
@@ -49,7 +45,11 @@ import org.apache.tinkerpop.gremlin.server.OpProcessor;
 import org.apache.tinkerpop.gremlin.server.op.AbstractEvalOpProcessor;
 import org.apache.tinkerpop.gremlin.server.op.OpProcessorException;
 import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.util.Tokens;
 import org.apache.tinkerpop.gremlin.util.function.ThrowingConsumer;
+import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
+import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
 import org.opencypher.gremlin.translation.CypherAst;
 import org.opencypher.gremlin.translation.groovy.GroovyPredicate;
 import org.opencypher.gremlin.translation.ir.TranslationWriter;
@@ -66,7 +66,7 @@ import scala.collection.Seq;
 /**
  * Description of the modifications:
  * <p>
- * 1) Changed the method signature to adopt the gremlin-server 3.5.1.
+ * 1) Changed the method signature to adopt the gremlin-server Context API.
  * <pre>
  * public Optional<ThrowingConsumer<Context>> selectOther(RequestMessage requestMessage)
  * -->
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
index ddb7c1a98..f58404646 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
@@ -57,7 +57,6 @@ import org.apache.hugegraph.util.Log;
 import org.apache.hugegraph.util.SafeDateUtil;
 import org.apache.tinkerpop.gremlin.process.traversal.Path;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
-import org.apache.tinkerpop.gremlin.structure.Element;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONIo;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.TinkerPopJacksonModule;
@@ -103,12 +102,14 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
         TYPE_DEFINITIONS = new ConcurrentHashMap<>();
 
         TYPE_DEFINITIONS.put(Optional.class, "Optional");
+        TYPE_DEFINITIONS.put(File.class, "File");
         TYPE_DEFINITIONS.put(Date.class, "Date");
         TYPE_DEFINITIONS.put(UUID.class, "UUID");
 
         // HugeGraph id serializer
         TYPE_DEFINITIONS.put(StringId.class, "StringId");
         TYPE_DEFINITIONS.put(LongId.class, "LongId");
+        TYPE_DEFINITIONS.put(UuidId.class, "UuidId");
         TYPE_DEFINITIONS.put(EdgeId.class, "EdgeId");
 
         // HugeGraph schema serializer
@@ -171,6 +172,7 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
         module.addSerializer(Shard.class, new ShardSerializer());
 
         module.addSerializer(File.class, new FileSerializer());
+        module.addDeserializer(File.class, new FileDeserializer());
 
         boolean useTimestamp = false;
         module.addSerializer(Date.class,
@@ -222,7 +224,9 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
          */
         module.addSerializer(HugeVertex.class, new HugeVertexSerializer());
         module.addSerializer(HugeEdge.class, new HugeEdgeSerializer());
+    }
 
+    public static void registerTraversalSerializers(SimpleModule module) {
         module.addSerializer(Path.class, new PathSerializer());
         module.addSerializer(Tree.class, new TreeSerializer());
     }
@@ -641,8 +645,8 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
                 String idValue = ctxt.readValue(jsonParser, String.class);
                 return (T) IdGenerator.of(idValue);
             } else if (clazz.equals(UuidId.class)) {
-                UUID idValue = ctxt.readValue(jsonParser, UUID.class);
-                return (T) IdGenerator.of(idValue);
+                String idValue = ctxt.readValue(jsonParser, String.class);
+                return (T) IdGenerator.of(UUID.fromString(idValue));
             } else {
                 assert clazz.equals(EdgeId.class);
                 String idValue = ctxt.readValue(jsonParser, String.class);
@@ -883,9 +887,8 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
         public void serialize(Tree tree, JsonGenerator jsonGenerator,
                               SerializerProvider provider) throws IOException {
             jsonGenerator.writeStartArray();
-            @SuppressWarnings("unchecked")
-            Set<Map.Entry<Element, Tree>> set = tree.entrySet();
-            for (Map.Entry<Element, Tree> entry : set) {
+            for (Object item : tree.entrySet()) {
+                Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
                 jsonGenerator.writeStartObject();
                 jsonGenerator.writeObjectField(GraphSONTokens.KEY,
                                                entry.getKey());
@@ -924,9 +927,65 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
         public void serialize(File file, JsonGenerator jsonGenerator,
                               SerializerProvider provider) throws IOException {
             jsonGenerator.writeStartObject();
-            jsonGenerator.writeStringField("file", file.getName());
+            this.writeFields(file, jsonGenerator);
             jsonGenerator.writeEndObject();
         }
+
+        @Override
+        public void serializeWithType(File file,
+                                      JsonGenerator jsonGenerator,
+                                      SerializerProvider provider,
+                                      TypeSerializer typeSer)
+                throws IOException {
+            WritableTypeId typeId = typeSer.typeId(
+                    file, JsonToken.VALUE_EMBEDDED_OBJECT);
+            typeSer.writeTypePrefix(jsonGenerator, typeId);
+            this.serialize(file, jsonGenerator, provider);
+            typeSer.writeTypeSuffix(jsonGenerator, typeId);
+        }
+
+        private void writeFields(File file, JsonGenerator jsonGenerator)
+                throws IOException {
+            jsonGenerator.writeStringField("file", file.getName());
+        }
+    }
+
+    private static class FileDeserializer extends StdDeserializer<File> {
+
+        public FileDeserializer() {
+            super(File.class);
+        }
+
+        @Override
+        public File deserialize(JsonParser jsonParser,
+                                DeserializationContext ctxt)
+                throws IOException {
+            JsonToken token = jsonParser.currentToken();
+            if (token == null) {
+                token = jsonParser.nextToken();
+            }
+            if (token == JsonToken.VALUE_STRING) {
+                return new File(jsonParser.getValueAsString());
+            }
+            if (token == JsonToken.START_OBJECT) {
+                String file = null;
+                while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+                    String field = jsonParser.currentName();
+                    jsonParser.nextToken();
+                    if ("file".equals(field)) {
+                        file = jsonParser.getValueAsString();
+                    } else {
+                        jsonParser.skipChildren();
+                    }
+                }
+                if (file == null) {
+                    return (File) ctxt.handleUnexpectedToken(File.class,
+                                                             jsonParser);
+                }
+                return new File(file);
+            }
+            return (File) ctxt.handleUnexpectedToken(File.class, jsonParser);
+        }
     }
 
     private static class BlobSerializer extends StdSerializer<Blob> {
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java
new file mode 100644
index 000000000..f46e4ee39
--- /dev/null
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * 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.hugegraph.io;
+
+import java.io.IOException;
+
+import org.apache.hugegraph.backend.id.Id;
+import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
+import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
+import org.apache.tinkerpop.gremlin.structure.io.binary.types.SimpleTypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.types.TransformSerializer;
+
+public final class HugeGraphTypeSerializerRegistryBuilder
+        extends TypeSerializerRegistry.Builder {
+
+    private static final TypeSerializer<Id> ID_TRANSFORM_SERIALIZER =
+            new IdTransformSerializer();
+
+    public HugeGraphTypeSerializerRegistryBuilder() {
+        this.withFallbackResolver(type -> {
+            if (Id.class.isAssignableFrom(type)) {
+                return ID_TRANSFORM_SERIALIZER;
+            }
+            return null;
+        });
+    }
+
+    private static final class IdTransformSerializer
+            extends SimpleTypeSerializer<Id>
+            implements TransformSerializer<Id> {
+
+        private IdTransformSerializer() {
+            super(null);
+        }
+
+        @Override
+        protected Id readValue(Buffer buffer, GraphBinaryReader context)
+                throws IOException {
+            throw new IOException("HugeGraph Id is written as a wire primitive");
+        }
+
+        @Override
+        protected void writeValue(Id value, Buffer buffer,
+                                  GraphBinaryWriter context)
+                throws IOException {
+            throw new IOException("HugeGraph Id is written as a wire primitive");
+        }
+
+        @Override
+        public Object transform(Id value) {
+            return value.asObject();
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
index fd00816f6..b2fce8da5 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
@@ -52,6 +52,7 @@ public final class JsonUtil {
         HugeGraphSONModule.registerServiceSerializers(module);
         HugeGraphSONModule.registerGraphSpaceSerializers(module);
         HugeGraphSONModule.registerGraphSerializers(module);
+        HugeGraphSONModule.registerTraversalSerializers(module);
 
         MAPPER.registerModule(module);
 
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
index 3c3e3049f..3c111bae3 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
@@ -19,8 +19,11 @@ package org.apache.hugegraph.api;
 
 import static org.apache.hugegraph.testutil.Assert.assertContains;
 
+import java.util.List;
 import java.util.Map;
 
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.util.JsonUtil;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -72,13 +75,153 @@ public class CypherApiTest extends BaseApiTest {
         this.testCypherQueryAndContains(cypher, "friend");
     }
 
-    private void testCypherQueryAndContains(String cypher, String containsText) {
+    @Test
+    public void testReturnNodeIdAsPrimitiveValue() {
+        String cypher = "MATCH (n:person) WHERE n.name = 'marko' " +
+                        "RETURN id(n) AS nodeId";
+
+        String content = this.testCypherQueryAndContains(cypher, "nodeId");
+        List<?> data = assertCypherSuccessData(content);
+        Map<?, ?> row = assertSingleMapRow(data);
+        Object nodeId = row.get("nodeId");
+
+        Assert.assertNotNull(nodeId);
+        assertPrimitiveValue(nodeId);
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnNodeDoesNotLeakInternalIdTypes() {
+        String cypher = "MATCH (n:person) WHERE n.name = 'marko' RETURN n";
+
+        String content = this.testCypherQueryAndContains(cypher, "marko");
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnNestedIdDoesNotLeakInternalIdTypes() {
+        String cypher = "MATCH (n:person) WHERE n.name = 'marko' " +
+                        "RETURN {nodeId: id(n), values: [id(n), n.name]} " +
+                        "AS payload";
+
+        String content = this.testCypherQueryAndContains(cypher, "payload");
+        List<?> data = assertCypherSuccessData(content);
+        Map<?, ?> row = assertSingleMapRow(data);
+        Map<?, ?> payload = assertMapValue(row, "payload");
+        List<?> values = assertListValue(payload, "values");
+        Object nodeId = payload.get("nodeId");
+
+        Assert.assertNotNull(nodeId);
+        assertPrimitiveValue(nodeId);
+        Assert.assertEquals(2, values.size());
+        Assert.assertEquals(nodeId, values.get(0));
+        Assert.assertEquals("marko", values.get(1));
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnRelationIdDoesNotLeakInternalIdTypes() {
+        String cypher = "MATCH (n:person)-[r:knows]->(friend:person) " +
+                        "WHERE n.name = 'marko' RETURN id(r) AS relationId";
+
+        String content = this.testCypherQueryAndContains(cypher, "relationId");
+        List<?> data = assertCypherSuccessData(content);
+        Map<?, ?> row = assertSingleMapRow(data);
+        Object relationId = row.get("relationId");
+
+        Assert.assertNotNull(relationId);
+        assertPrimitiveValue(relationId);
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnPathShape() {
+        String cypher = "MATCH p=(n:person)-[r:knows]->(friend:person) " +
+                        "WHERE n.name = 'marko' RETURN p AS path";
+
+        String content = this.testCypherQueryAndContains(cypher, "path");
+        List<?> data = assertCypherSuccessData(content);
+        Map<?, ?> row = assertSingleMapRow(data);
+        List<?> path = assertListValue(row, "path");
+
+        Assert.assertEquals(3, path.size());
+        Map<?, ?> source = assertMapValue(path, 0);
+        Map<?, ?> relation = assertMapValue(path, 1);
+        Map<?, ?> target = assertMapValue(path, 2);
+
+        Assert.assertEquals("node", source.get("_type"));
+        Assert.assertEquals("person", source.get("_label"));
+        Assert.assertEquals("marko", source.get("name"));
+        Assert.assertEquals("knows", relation.get("_label"));
+        Assert.assertEquals("node", target.get("_type"));
+        Assert.assertEquals("person", target.get("_label"));
+        Assert.assertEquals("peter", target.get("name"));
+        assertContains("marko", content);
+        assertContains("peter", content);
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    private String testCypherQueryAndContains(String cypher,
+                                             String containsText) {
         Response r = client().post(PATH, cypher);
-        this.validStatusAndTextContains(containsText, r);
+        return this.validStatusAndTextContains(containsText, r);
     }
 
-    private void validStatusAndTextContains(String value, Response r) {
+    private String validStatusAndTextContains(String value, Response r) {
         String content = assertResponseStatus(200, r);
         assertContains(value, content);
+        return content;
+    }
+
+    private static void assertNoHugeGraphIdLeak(String content) {
+        Assert.assertFalse(content.contains("org.apache.hugegraph.backend.id"));
+        Assert.assertFalse(content.contains("StringId"));
+        Assert.assertFalse(content.contains("LongId"));
+        Assert.assertFalse(content.contains("UuidId"));
+        Assert.assertFalse(content.contains("EdgeId"));
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<?> assertCypherSuccessData(String content) {
+        Map<?, ?> response = JsonUtil.fromJson(content, Map.class);
+        Assert.assertTrue(response.containsKey("requestId"));
+
+        Map<?, ?> status = assertMapValue(response, "status");
+        Assert.assertEquals(200, ((Number) status.get("code")).intValue());
+        Assert.assertEquals("", status.get("message"));
+
+        Map<?, ?> result = assertMapValue(response, "result");
+        Assert.assertInstanceOf(List.class, result.get("data"));
+        Assert.assertInstanceOf(Map.class, result.get("meta"));
+        return (List<?>) result.get("data");
+    }
+
+    private static Map<?, ?> assertSingleMapRow(List<?> data) {
+        Assert.assertEquals(1, data.size());
+        Assert.assertInstanceOf(Map.class, data.get(0));
+        return (Map<?, ?>) data.get(0);
+    }
+
+    private static Map<?, ?> assertMapValue(Map<?, ?> map, String key) {
+        Assert.assertTrue(map.containsKey(key));
+        Assert.assertInstanceOf(Map.class, map.get(key));
+        return (Map<?, ?>) map.get(key);
+    }
+
+    private static Map<?, ?> assertMapValue(List<?> list, int index) {
+        Assert.assertTrue(list.size() > index);
+        Assert.assertInstanceOf(Map.class, list.get(index));
+        return (Map<?, ?>) list.get(index);
+    }
+
+    private static List<?> assertListValue(Map<?, ?> map, String key) {
+        Assert.assertTrue(map.containsKey(key));
+        Assert.assertInstanceOf(List.class, map.get(key));
+        return (List<?>) map.get(key);
+    }
+
+    private static void assertPrimitiveValue(Object value) {
+        Assert.assertFalse(value instanceof Map);
+        Assert.assertFalse(value instanceof List);
     }
 }
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java
new file mode 100644
index 000000000..d9d36638d
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.hugegraph.api.cypher;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.junit.Test;
+
+public class CypherClientTest extends BaseUnitTest {
+
+    @Test
+    public void testNormalizeHandlesNullMapAndArrayValues() {
+        Map<Object, Object> value = new LinkedHashMap<>();
+        value.put(IdGenerator.of(1L),
+                  new Object[]{IdGenerator.of("marko"), null});
+
+        Object normalized = CypherClient.normalize(value);
+
+        Assert.assertInstanceOf(Map.class, normalized);
+        Map<?, ?> map = (Map<?, ?>) normalized;
+        Assert.assertTrue(map.containsKey(1L));
+        Assert.assertInstanceOf(List.class, map.get(1L));
+
+        List<?> values = (List<?>) map.get(1L);
+        Assert.assertEquals("marko", values.get(0));
+        Assert.assertNull(values.get(1));
+    }
+
+    @Test
+    public void testNormalizeHandlesCyclicReferences() {
+        Map<String, Object> value = new LinkedHashMap<>();
+        value.put("private-value", value);
+
+        Assert.assertThrows(IllegalArgumentException.class,
+                            () -> CypherClient.normalize(value), e -> {
+            Assert.assertContains("cyclic Cypher result", e.getMessage());
+            Assert.assertFalse(e.getMessage().contains("private-value"));
+        });
+    }
+
+    @Test
+    public void testNormalizePreservesThirtyTwoContainerLayers() {
+        Object value = "leaf";
+        for (int i = 0; i < 32; i++) {
+            value = new Object[]{value};
+        }
+
+        Object normalized = CypherClient.normalize(value);
+        Object current = normalized;
+        for (int i = 0; i < 32; i++) {
+            Assert.assertInstanceOf(List.class, current);
+            List<?> list = (List<?>) current;
+            Assert.assertEquals(1, list.size());
+            current = list.get(0);
+        }
+
+        Assert.assertEquals("leaf", current);
+    }
+
+    @Test
+    public void testNormalizeRejectsThirtyThirdContainerLayer() {
+        Object value = "leaf";
+        for (int i = 0; i < 33; i++) {
+            value = new Object[]{value};
+        }
+        Object deeplyNestedValue = value;
+
+        Assert.assertThrows(IllegalArgumentException.class,
+                            () -> CypherClient.normalize(deeplyNestedValue),
+                            e -> Assert.assertContains(
+                                    "max normalization depth 32",
+                                    e.getMessage()));
+    }
+
+    @Test
+    public void testNormalizePreservesPathLabelsAndObjects() {
+        Path path = MutablePath.make()
+                               .extend(IdGenerator.of("marko"),
+                                       Set.of("a"))
+                               .extend(IdGenerator.of("lop"),
+                                       Set.of("b", "software"));
+
+        Object normalized = CypherClient.normalize(path);
+
+        Assert.assertInstanceOf(Map.class, normalized);
+        Map<?, ?> map = (Map<?, ?>) normalized;
+        Assert.assertTrue(map.containsKey("labels"));
+        Assert.assertTrue(map.containsKey("objects"));
+
+        Assert.assertInstanceOf(List.class, map.get("labels"));
+        Assert.assertInstanceOf(List.class, map.get("objects"));
+
+        List<?> labels = (List<?>) map.get("labels");
+        List<?> objects = (List<?>) map.get("objects");
+        Assert.assertEquals(2, labels.size());
+        Assert.assertEquals(2, objects.size());
+
+        Assert.assertEquals("marko", objects.get(0));
+        Assert.assertEquals("lop", objects.get(1));
+        List<?> firstLabels = (List<?>) labels.get(0);
+        List<?> secondLabels = (List<?>) labels.get(1);
+        Assert.assertTrue(firstLabels.contains("a"));
+        Assert.assertTrue(secondLabels.contains("b"));
+        Assert.assertTrue(secondLabels.contains("software"));
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java
new file mode 100644
index 000000000..5b7b5fa4a
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.hugegraph.unit.serializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.hugegraph.backend.id.EdgeId;
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.backend.id.IdGenerator.LongId;
+import org.apache.hugegraph.backend.id.IdGenerator.StringId;
+import org.apache.hugegraph.backend.id.IdGenerator.UuidId;
+import org.apache.hugegraph.io.HugeGraphIoRegistry;
+import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.unit.FakeObjects;
+import org.apache.hugegraph.util.JsonUtil;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONReader;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONWriter;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.TypeInfo;
+import org.junit.Test;
+
+public class HugeGraphSONModuleTest extends BaseUnitTest {
+
+    @Test
+    public void testSerializeFileWithGraphSONTypeInfo() throws IOException {
+        String json = writeTyped(new File("test.text"));
+        Map<?, ?> typedFile = JsonUtil.fromJson(json, Map.class);
+        Object value = typedFile.get("@value");
+
+        Assert.assertEquals("hugegraph:File", typedFile.get("@type"));
+        Assert.assertInstanceOf(Map.class, value);
+        Assert.assertEquals("test.text", ((Map<?, ?>) value).get("file"));
+        Assert.assertContains("hugegraph:File", json);
+        Assert.assertContains("\"file\"", json);
+        Assert.assertContains("test.text", json);
+
+        File file = readTyped(json, File.class);
+        Assert.assertEquals("test.text", file.getName());
+    }
+
+    @Test
+    public void testRoundTripIdWithGraphSONTypeInfo() throws IOException {
+        StringId expectedStringId = (StringId) IdGenerator.of("marko");
+        LongId expectedLongId = (LongId) IdGenerator.of(123L);
+        UuidId expectedUuidId = (UuidId) IdGenerator.of(
+                UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"));
+        EdgeId expectedEdgeId = EdgeId.parse("S1>2>3>4>L6");
+
+        String stringId = writeTyped(expectedStringId);
+        String longId = writeTyped(expectedLongId);
+        String uuidId = writeTyped(expectedUuidId);
+        String edgeId = writeTyped(expectedEdgeId);
+
+        Assert.assertContains("hugegraph:StringId", stringId);
+        Assert.assertContains("marko", stringId);
+        Assert.assertContains("hugegraph:LongId", longId);
+        Assert.assertContains("123", longId);
+        Assert.assertContains("hugegraph:UuidId", uuidId);
+        Assert.assertContains("3cfcafc8-7906-4ab7-a207-4ded056f58de",
+                              uuidId);
+        Assert.assertContains("hugegraph:EdgeId", edgeId);
+        Assert.assertContains("S1>2>3>4>L6", edgeId);
+
+        Assert.assertEquals(expectedStringId,
+                            readTyped(stringId, StringId.class));
+        Assert.assertEquals(expectedLongId,
+                            readTyped(longId, LongId.class));
+        Assert.assertEquals(expectedUuidId,
+                            readTyped(uuidId, UuidId.class));
+        Assert.assertEquals(expectedEdgeId,
+                            readTyped(edgeId, EdgeId.class));
+    }
+
+    @Test
+    public void testSerializeSchemaWithUntypedGraphSONModule() throws IOException {
+        FakeObjects objects = new FakeObjects();
+        PropertyKey propertyKey = objects.newPropertyKey(IdGenerator.of(1L),
+                                                         "name");
+
+        String json = writeUntyped(propertyKey);
+
+        Assert.assertContains("\"name\"", json);
+    }
+
+    private static String writeTyped(Object object) throws IOException {
+        GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES);
+        GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create();
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+        writer.writeObject(output, object);
+
+        return output.toString(StandardCharsets.UTF_8.name());
+    }
+
+    private static <T> T readTyped(String json, Class<T> clazz)
+            throws IOException {
+        GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES);
+        GraphSONReader reader = GraphSONReader.build().mapper(mapper).create();
+        ByteArrayInputStream input = new ByteArrayInputStream(
+                json.getBytes(StandardCharsets.UTF_8));
+
+        return reader.readObject(input, clazz);
+    }
+
+    private static String writeUntyped(Object object) throws IOException {
+        GraphSONMapper mapper = mapper(TypeInfo.NO_TYPES);
+        GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create();
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+        writer.writeObject(output, object);
+
+        return output.toString(StandardCharsets.UTF_8.name());
+    }
+
+    private static GraphSONMapper mapper(TypeInfo typeInfo) {
+        GraphSONMapper mapper = GraphSONMapper.build()
+                                              .version(GraphSONVersion.V3_0)
+                                              .typeInfo(typeInfo)
+                                              .addRegistry(HugeGraphIoRegistry.instance())
+                                              .create();
+
+        return mapper;
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
index c76d536ff..722f747ed 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
@@ -22,6 +22,7 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.UUID;
 
 import org.apache.commons.lang3.tuple.Pair;
@@ -51,6 +52,9 @@ import org.apache.hugegraph.unit.BaseUnitTest;
 import org.apache.hugegraph.unit.FakeObjects;
 import org.apache.hugegraph.util.JsonUtil;
 import org.apache.hugegraph.util.collection.CollectionFactory;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
 import org.apache.tinkerpop.shaded.jackson.core.type.TypeReference;
 import org.eclipse.collections.api.map.primitive.MutableIntObjectMap;
 import org.junit.Test;
@@ -308,6 +312,31 @@ public class JsonUtilTest extends BaseUnitTest {
                             "\"weight\":0.8}}", json);
     }
 
+    @Test
+    public void testSerializePath() {
+        Path path = MutablePath.make()
+                               .extend("marko", Set.of("a"))
+                               .extend(29, Set.of("b"));
+
+        String json = JsonUtil.toJson(path);
+
+        Assert.assertEquals("{\"labels\":[[\"a\"],[\"b\"]]," +
+                            "\"objects\":[\"marko\",29]}", json);
+    }
+
+    @Test
+    public void testSerializeTree() {
+        Tree<String> child = new Tree<>();
+        child.put("lop", new Tree<>());
+        Tree<String> tree = new Tree<>();
+        tree.put("marko", child);
+
+        String json = JsonUtil.toJson(tree);
+
+        Assert.assertEquals("[{\"key\":\"marko\",\"value\":[{" +
+                            "\"key\":\"lop\",\"value\":[]}]}]", json);
+    }
+
     @Test
     public void testDeserializeList() {
         String json = "[\"1\", \"2\", \"3\"]";