You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2025/08/29 12:45:39 UTC

(tinkerpop) branch TINKERPOP-3182 created (now a74d627d93)

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

spmallette pushed a change to branch TINKERPOP-3182
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


      at a74d627d93 TINKERPOP-3182 Fixed inconsistency in Path GraphSON serialization

This branch includes the following new commits:

     new a74d627d93 TINKERPOP-3182 Fixed inconsistency in Path GraphSON serialization

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(tinkerpop) 01/01: TINKERPOP-3182 Fixed inconsistency in Path GraphSON serialization

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch TINKERPOP-3182
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit a74d627d932053c5fc8bcc355b6f9b1246c7943b
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Fri Aug 22 15:38:24 2025 -0400

    TINKERPOP-3182 Fixed inconsistency in Path GraphSON serialization
    
    Path objects were being forcibly detached by reference in GraphSON v1/v2 which put it at odds with v1 untyped. Inclusion or removal of properties should be controlled via materializeProperties only.
---
 CHANGELOG.asciidoc                                 |  2 +
 .../structure/io/binary/types/PathSerializer.java  |  4 +-
 .../io/graphson/GraphSONSerializersV2.java         | 11 ++-
 .../io/graphson/GraphSONSerializersV3.java         | 10 ++-
 .../DriverRemoteConnection/GraphTraversalTests.cs  | 47 +++++++++++
 gremlin-go/driver/connection_test.go               | 54 ++++++++++++
 .../gremlin-javascript/lib/structure/graph.js      | 17 +++-
 .../test/integration/client-tests.js               | 70 ++++++++++++++--
 .../test/integration/traversal-test.js             | 73 +++++++++++++++++
 .../test/unit/graphbinary/PathSerializer-test.js   | 92 +++++++++++++++++++++
 .../gremlin-javascript/test/unit/graphson-test.js  | 38 +++++++--
 .../tests/driver/test_driver_remote_connection.py  | 15 ++++
 .../apache/tinkerpop/gremlin/server/Context.java   |  8 +-
 .../GremlinServerSerializationIntegrateTest.java   | 95 +++++++++++++++++++++-
 .../gremlin/structure/SerializationTest.java       | 29 +++----
 .../org/apache/tinkerpop/gremlin/util/Tokens.java  |  5 +-
 16 files changed, 516 insertions(+), 54 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 737aa81dd2..15b6a6858e 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -111,6 +111,8 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 
 * Improved Java driver host availability on connection pool initialization.
 * Added getter for `parameterItems` and `valueTraversal` on `DifferenceStep`.
+* Added properties to `Element` objects found in a `Path` for GraphSON v2 and v3 and GraphBinary.
+* Fixed edge properties for GraphBinary which were not deserializing properly.
 
 [[release-3-7-4]]
 === TinkerPop 3.7.4 (Release Date: August 1, 2025)
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PathSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PathSerializer.java
index b59436c8e0..830ffdc4e0 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PathSerializer.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PathSerializer.java
@@ -52,7 +52,9 @@ public class PathSerializer extends SimpleTypeSerializer<Path> {
             path.extend(objects.get(ix), labels.get(ix));
         }
 
-        return ReferenceFactory.detach(path);
+        // before 3.7.5, we forced detachment to reference here. better to just allow materializeProperties to control
+        // detachment or not.
+        return path;
     }
 
     @Override
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV2.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV2.java
index e633efb49d..a36d3d9a0b 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV2.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV2.java
@@ -258,10 +258,15 @@ class GraphSONSerializersV2 {
                 throws IOException, JsonGenerationException {
             jsonGenerator.writeStartObject();
 
+            // prior to 3.7.5, the code was such that:
             // paths shouldn't serialize with properties if the path contains graph elements
-            final Path p = DetachedFactory.detach(path, false);
-            jsonGenerator.writeObjectField(GraphSONTokens.LABELS, p.labels());
-            jsonGenerator.writeObjectField(GraphSONTokens.OBJECTS, p.objects());
+            //
+            // however, there was the idea that v2 untyped, with just a couple documented exceptions, should essentially
+            // match v1 which does include the properties (Path objects are not documented as exceptions). as of 3.7.5,
+            // we remove detachment to references and allow users to control the inclusion or exclusion of properties
+            // with the materializeProperties option.
+            jsonGenerator.writeObjectField(GraphSONTokens.LABELS, path.labels());
+            jsonGenerator.writeObjectField(GraphSONTokens.OBJECTS, path.objects());
 
             jsonGenerator.writeEndObject();
         }
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV3.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV3.java
index 2d9024aa60..c8a7cf2dc5 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV3.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV3.java
@@ -281,10 +281,14 @@ class GraphSONSerializersV3 {
                 throws IOException, JsonGenerationException {
             jsonGenerator.writeStartObject();
 
+            // prior to 3.7.5, the code was such that:
             // paths shouldn't serialize with properties if the path contains graph elements
-            final Path p = DetachedFactory.detach(path, false);
-            jsonGenerator.writeObjectField(GraphSONTokens.LABELS, p.labels());
-            jsonGenerator.writeObjectField(GraphSONTokens.OBJECTS, p.objects());
+            //
+            // however, there was the idea that v3 untyped should essentially match v1 which does include the
+            // properties. as of 3.7.5, we remove detachment to references and allow users to control the inclusion
+            // or exclusion of properties with the materializeProperties option.
+            jsonGenerator.writeObjectField(GraphSONTokens.LABELS, path.labels());
+            jsonGenerator.writeObjectField(GraphSONTokens.OBJECTS, path.objects());
 
             jsonGenerator.writeEndObject();
         }
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
index 3fde6ba121..ed028a804f 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
@@ -333,5 +333,52 @@ namespace Gremlin.Net.IntegrationTest.Process.Traversal.DriverRemoteConnection
                 Assert.True(vp.Properties == null || vp.Properties.Length == 0);
             }
         }
+        
+        [Fact]
+        public void shouldUseMaterializedPropertiesTokenInPath()
+        {
+            var connection = _connectionFactory.CreateRemoteConnection();
+            var g = AnonymousTraversalSource.Traversal().WithRemote(connection);
+            var p = g.With("materializeProperties", "tokens").V().Has("name", "marko").OutE().InV().HasLabel("software").Path().Next();
+            
+            Assert.NotNull(p);
+            Assert.Equal(3, p.Count);
+            
+            var a = p[0] as Vertex;
+            var b = p[1] as Edge;
+            var c = p[2] as Vertex;
+            
+            Assert.NotNull(a);
+            Assert.NotNull(b);
+            Assert.NotNull(c);
+            
+            // GraphSON will deserialize into null and GraphBinary to []
+            Assert.True(a.Properties == null || a.Properties.Length == 0);
+            Assert.True(b.Properties == null || b.Properties.Length == 0);
+            Assert.True(c.Properties == null || c.Properties.Length == 0);
+        }
+        
+        [Fact]
+        public void shouldMaterializePropertiesAllInPath()
+        {
+            var connection = _connectionFactory.CreateRemoteConnection();
+            var g = AnonymousTraversalSource.Traversal().WithRemote(connection);
+            var p = g.With("materializeProperties", "all").V().Has("name", "marko").OutE().InV().HasLabel("software").Path().Next();
+
+            Assert.NotNull(p);
+            Assert.Equal(3, p.Count);
+
+            var a = p[0] as Vertex;
+            var b = p[1] as Edge;
+            var c = p[2] as Vertex;
+
+            Assert.NotNull(a);
+            Assert.NotNull(b);
+            Assert.NotNull(c);
+
+            Assert.True(a.Properties != null && a.Properties.Length > 0);
+            Assert.True(b.Properties != null && b.Properties.Length > 0);
+            Assert.True(c.Properties != null && c.Properties.Length > 0);
+        }
     }
 }
\ No newline at end of file
diff --git a/gremlin-go/driver/connection_test.go b/gremlin-go/driver/connection_test.go
index fc5053366c..153084dbb7 100644
--- a/gremlin-go/driver/connection_test.go
+++ b/gremlin-go/driver/connection_test.go
@@ -1266,5 +1266,59 @@ func TestConnection(t *testing.T) {
 			assert.True(t, ok)
 			assert.Equal(t, 0, len(properties))
 		}
+
+		// Path elements should also have no materialized properties when tokens is set
+		r, err := g.With("materializeProperties", MaterializeProperties.Tokens).V().Has("person", "name", "marko").OutE().InV().HasLabel("software").Path().Next()
+		assert.Nil(t, err)
+		p, err := r.GetPath()
+		assert.Nil(t, err)
+		assert.NotNil(t, p)
+		assert.Equal(t, 3, len(p.Objects))
+
+		// first element should be a Vertex
+		if a, ok := p.Objects[0].(*Vertex); assert.True(t, ok) {
+			props, ok := a.Properties.([]interface{})
+			assert.True(t, ok)
+			assert.Equal(t, 0, len(props))
+		}
+		// second element should be an Edge
+		if b, ok := p.Objects[1].(*Edge); assert.True(t, ok) {
+			props, ok := b.Properties.([]interface{})
+			assert.True(t, ok)
+			assert.Equal(t, 0, len(props))
+		}
+		// third element should be a Vertex
+		if c, ok := p.Objects[2].(*Vertex); assert.True(t, ok) {
+			props, ok := c.Properties.([]interface{})
+			assert.True(t, ok)
+			assert.Equal(t, 0, len(props))
+		}
+
+		// Path elements should have materialized properties when all is set
+		r, err = g.With("materializeProperties", MaterializeProperties.All).V().Has("person", "name", "marko").OutE().InV().HasLabel("software").Path().Next()
+		assert.Nil(t, err)
+		p, err = r.GetPath()
+		assert.Nil(t, err)
+		assert.NotNil(t, p)
+		assert.Equal(t, 3, len(p.Objects))
+
+		// first element should be a Vertex with properties present
+		if a, ok := p.Objects[0].(*Vertex); assert.True(t, ok) {
+			props, ok := a.Properties.([]interface{})
+			assert.True(t, ok)
+			assert.Greater(t, len(props), 0)
+		}
+		// second element should be an Edge with properties present
+		if b, ok := p.Objects[1].(*Edge); assert.True(t, ok) {
+			props, ok := b.Properties.([]interface{})
+			assert.True(t, ok)
+			assert.Greater(t, len(props), 0)
+		}
+		// third element should be a Vertex with properties present
+		if c, ok := p.Objects[2].(*Vertex); assert.True(t, ok) {
+			props, ok := c.Properties.([]interface{})
+			assert.True(t, ok)
+			assert.Greater(t, len(props), 0)
+		}
 	})
 }
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js
index 4171c3556e..9e0b19f30f 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/graph.js
@@ -79,10 +79,19 @@ class Edge extends Element {
     this.inV = inV;
     this.properties = {};
     if (properties) {
-      const keys = Object.keys(properties);
-      for (let i = 0; i < keys.length; i++) {
-        const k = keys[i];
-        this.properties[k] = properties[k].value;
+      if (Array.isArray(properties)) {
+        // Handle array of Property objects
+        for (let i = 0; i < properties.length; i++) {
+          const prop = properties[i];
+          this.properties[prop.key] = prop.value;
+        }
+      } else {
+        // Handle object format as before
+        const keys = Object.keys(properties);
+        for (let i = 0; i < keys.length; i++) {
+          const k = keys[i];
+          this.properties[k] = properties[k].value;
+        }
       }
     }
   }
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/client-tests.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/client-tests.js
index a52b92ff6a..a61701d255 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/client-tests.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/client-tests.js
@@ -25,14 +25,16 @@ const graphModule = require('../../lib/structure/graph');
 const helper = require('../helper');
 const t = require('../../lib/process/traversal');
 
-let client;
+let client, clientCrew;
 
 describe('Client', function () {
   before(function () {
     client = helper.getClient('gmodern');
+    clientCrew = helper.getClient('gcrew')
     return client.open();
   });
   after(function () {
+    clientCrew.close();
     return client.close();
   });
   describe('#submit()', function () {
@@ -84,12 +86,16 @@ describe('Client', function () {
           assert.ok(vertex instanceof graphModule.Vertex);
           let age, name
           if (vertex.properties instanceof Array) {
-            age = vertex.properties[1]
-            name = vertex.properties[0]
+            const ageProps = vertex.properties.filter(p => p.key === 'age');
+            const nameProps = vertex.properties.filter(p => p.key === 'name');
+            age = ageProps[0];
+            name = nameProps[0];
           } else {
             age = vertex.properties.age[0]
             name = vertex.properties.name[0]
           }
+          assert.ok(age);
+          assert.ok(name);
           assert.strictEqual(age.value, 29);
           assert.strictEqual(name.value, 'marko');
         });
@@ -113,19 +119,63 @@ describe('Client', function () {
           assert.strictEqual(result.length, 1);
           const vertex = result.first();
           assert.ok(vertex instanceof graphModule.Vertex);
+          // if/then until TINKERPOP-3186
           let age, name
           if (vertex.properties instanceof Array) {
-            age = vertex.properties[1]
-            name = vertex.properties[0]
+            const ageProps = vertex.properties.filter(p => p.key === 'age');
+            const nameProps = vertex.properties.filter(p => p.key === 'name');
+            age = ageProps[0];
+            name = nameProps[0];
           } else {
             age = vertex.properties.age[0]
             name = vertex.properties.name[0]
           }
+          assert.ok(age);
+          assert.ok(name);
           assert.strictEqual(age.value, 29);
           assert.strictEqual(name.value, 'marko');
         });
     });
 
+    it('should handle Edge properties for gremlin request', function () {
+      return client.submit('g.E().has("weight", 0.5).limit(1)')
+        .then(function (result) {
+          assert.ok(result);
+          assert.strictEqual(result.length, 1);
+          const edge = result.first();
+          assert.ok(edge instanceof graphModule.Edge);
+          assert.strictEqual(edge.label, 'knows');
+          assert.strictEqual(edge.properties.weight, 0.5);
+          assert.ok(edge.inV);
+          assert.ok(edge.outV);
+        });
+    });
+
+    it('should handle VertexProperty metadata for gremlin request', function () {
+      return clientCrew.submit('g.V(7).properties("location").limit(1)')
+        .then(function (result) {
+          assert.ok(result);
+          assert.strictEqual(result.length, 1);
+          const prop = result.first();
+          assert.ok(prop instanceof graphModule.VertexProperty);
+          assert.strictEqual(prop.key, 'location');
+          assert.strictEqual(prop.value, 'centreville');
+
+          // Check meta-properties - TINKERPOP-3186
+          if (prop.properties instanceof Object && !(prop.properties instanceof Array)) {
+            assert.strictEqual(prop.properties.startTime, 1990);
+            assert.strictEqual(prop.properties.endTime, 2000);
+          } else {
+            const startTime = prop.properties.find(p => p.key === 'startTime');
+            const endTime = prop.properties.find(p => p.key === 'endTime');
+            assert.ok(startTime);
+            assert.ok(endTime);
+            assert.strictEqual(startTime.value, 1990);
+            assert.strictEqual(endTime.value, 2000);
+          }
+        });
+    });
+
     it('should skip Vertex properties for gremlin request with tokens', function () {
       return client.submit('g.with("materializeProperties", "tokens").V(1)')
         .then(function (result) {
@@ -306,10 +356,12 @@ function assertVertexProperties(vertex) {
   const vertexProperty = locations[0];
   assert.strictEqual(vertexProperty.value, 'centreville');
   if (vertexProperty.properties instanceof Array) {
-    assert.strictEqual(vertexProperty.properties[0].key, 'startTime');
-    assert.strictEqual(vertexProperty.properties[0].value, 1990);
-    assert.strictEqual(vertexProperty.properties[1].key, 'endTime');
-    assert.strictEqual(vertexProperty.properties[1].value, 2000);
+    const start = vertexProperty.properties.find(p => p.key === 'startTime');
+    const end = vertexProperty.properties.find(p => p.key === 'endTime');
+    assert.ok(start);
+    assert.ok(end);
+    assert.strictEqual(start.value, 1990);
+    assert.strictEqual(end.value, 2000);
   } else {
     assert.strictEqual(vertexProperty.properties.startTime, 1990);
     assert.strictEqual(vertexProperty.properties.endTime, 2000);
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js
index 3ff69165c2..89b4c85232 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js
@@ -159,6 +159,79 @@ describe('Traversal', function () {
         list.forEach(vp => assert.ok(vp.properties === undefined || vp.properties.length === 0));
       });
     });
+    it('should skip path element properties when tokens is set', function () {
+      var g = traversal().withRemote(connection);
+      return g.with_("materializeProperties", "tokens").V().has('name','marko').outE().inV().hasLabel('software').path().next().then(function (item) {
+        const p = item.value;
+        assert.ok(p);
+        assert.strictEqual(p.objects.length, 3);
+        const a = p.objects[0];
+        const b = p.objects[1];
+        const c = p.objects[2];
+        assert.ok(a instanceof Vertex);
+        assert.ok(b instanceof Edge);
+        assert.ok(c instanceof Vertex);
+        assert.ok(a.properties === undefined || a.properties.length === 0);
+        assert.strictEqual(Object.keys(b.properties).length, 0);
+        assert.ok(c.properties === undefined || c.properties.length === 0);
+      });
+    });
+    it('should materialize path element properties when all is set', function () {
+      var g = traversal().withRemote(connection);
+      return g.with_("materializeProperties", "all").V().has('name','marko').outE().inV().hasLabel('software').path().next().then(function (item) {
+        const p = item.value;
+        assert.ok(p);
+        assert.strictEqual(p.objects.length, 3);
+        const a = p.objects[0];
+        const b = p.objects[1];
+        const c = p.objects[2];
+        assert.ok(a instanceof Vertex);
+        assert.ok(b instanceof Edge);
+        assert.ok(c instanceof Vertex);
+        assert.ok(a.properties);
+        // these assertions are if/then because of how mixed up javascript is right now on serialization
+        // standards as discussed on TINKERPOP-3186 - can fix that on a breaking change...until then we
+        // just retain existing weirdness.
+        let aNameProps, aAgeProps;
+        if (a.properties instanceof Array) {
+          aNameProps = a.properties.filter(p => p.key === 'name');
+          aAgeProps = a.properties.filter(p => p.key === 'age');
+        } else {
+          aNameProps = a.properties['name'];
+          aAgeProps = a.properties['age'];
+        }
+        assert.ok(aNameProps);
+        assert.strictEqual(aNameProps.length, 1);
+        assert.strictEqual(aNameProps[0].value, 'marko');
+        assert.ok(aAgeProps);
+        assert.strictEqual(aAgeProps.length, 1);
+        assert.strictEqual(aAgeProps[0].value, 29);
+        assert.ok(b.properties);
+        let bWeight;
+        if (b.properties instanceof Array) {
+          bWeight = b.properties.filter(p => p.key === 'weight');
+        } else {
+          bWeight = b.properties['weight'];
+        }
+        assert.ok(bWeight !== undefined);
+        assert.strictEqual(bWeight, 0.4);
+        assert.ok(c.properties);
+        let cNameProps, cLangProps;
+        if (c.properties instanceof Array) {
+          cNameProps = c.properties.filter(p => p.key === 'name');
+          cLangProps = c.properties.filter(p => p.key === 'lang');
+        } else {
+          cNameProps = c.properties['name'];
+          cLangProps = c.properties['lang'];
+        }
+        assert.ok(cNameProps);
+        assert.strictEqual(cNameProps.length, 1);
+        assert.strictEqual(cNameProps[0].value, 'lop');
+        assert.ok(cLangProps);
+        assert.strictEqual(cLangProps.length, 1);
+        assert.strictEqual(cLangProps[0].value, 'java');
+      });
+    });
   });
   describe('lambdas', function() {
     it('should handle 1-arg lambdas', function() {
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/PathSerializer-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/PathSerializer-test.js
index a65a842c5f..95198c4862 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/PathSerializer-test.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/PathSerializer-test.js
@@ -149,4 +149,96 @@ describe('GraphBinary.PathSerializer', () => {
     ))
   );
 
+  // Additional complex path tests with Vertex and Edge objects
+  describe('complex V-E-V path round-trip', () => {
+    function buildVertex(id, label, propsObj) {
+      if (!propsObj) return new g.Vertex(id, label, undefined);
+      // build array of VertexProperty entries as expected by Vertex
+      const vps = Object.keys(propsObj).map((k, idx) => new g.VertexProperty(idx + 1, k, propsObj[k], undefined));
+      return new g.Vertex(id, label, vps);
+    }
+
+    function buildEdge(id, outV, label, inV, propsObj) {
+      // Edge expects properties as a map of key -> g.Property (the constructor extracts .value)
+      let props = undefined;
+      if (propsObj) {
+        props = {};
+        for (const k of Object.keys(propsObj)) props[k] = new g.Property(k, propsObj[k]);
+      }
+      return new g.Edge(id, outV, label, inV, props);
+    }
+
+    it('should serialize/deserialize Path with V-E-V that includes properties', () => {
+      const vOut = buildVertex(1, 'person', { name: 'marko', age: 29 });
+      const vIn = buildVertex(3, 'software', { name: 'lop', lang: 'java' });
+      const e = buildEdge(9, vOut, 'created', vIn, { weight: 0.4 });
+
+      const p = new g.Path([[ 'a' ], [ 'b' ], [ 'c' ]], [ vOut, e, vIn ]);
+
+      // round-trip with non-fully-qualified format
+      const buf = pathSerializer.serialize(p, false);
+      const { v: deser, len } = pathSerializer.deserialize(buf, false);
+
+      // sanity on buffer consumed
+      assert.strictEqual(len, buf.length);
+
+      // assert labels
+      assert.strictEqual(deser.labels.length, 3);
+      assert.strictEqual(deser.labels[0].length, 1);
+      assert.strictEqual(deser.labels[1].length, 1);
+      assert.strictEqual(deser.labels[2].length, 1);
+
+      // assert objects and their types
+      assert.strictEqual(deser.objects.length, 3);
+      const a = deser.objects[0];
+      const b = deser.objects[1];
+      const c = deser.objects[2];
+      assert.ok(a instanceof g.Vertex);
+      assert.ok(b instanceof g.Edge);
+      assert.ok(c instanceof g.Vertex);
+
+      // Vertex properties should be present (VertexSerializer preserves them)
+      assert.ok(Array.isArray(a.properties));
+      assert.ok(a.properties.length > 0);
+      assert.ok(Array.isArray(c.properties));
+      assert.ok(c.properties.length > 0);
+
+      // Edge properties are not serialized in GraphBinary (EdgeSerializer writes null), expect empty object
+      assert.ok(b.properties);
+      assert.strictEqual(Object.keys(b.properties).length, 0);
+    });
+
+    it('should serialize/deserialize Path with V-E-V without properties', () => {
+      const vOut = buildVertex(1, 'person', undefined);
+      const vIn = buildVertex(3, 'software', undefined);
+      const e = buildEdge(9, vOut, 'created', vIn, undefined);
+
+      const p = new g.Path([[ 'a' ], [ 'b' ], [ 'c' ]], [ vOut, e, vIn ]);
+
+      // round-trip with fully-qualified format
+      const fqBuf = pathSerializer.serialize(p, true);
+      const { v: deser, len } = pathSerializer.deserialize(fqBuf, true);
+
+      assert.strictEqual(len, fqBuf.length);
+
+      // types
+      const a = deser.objects[0];
+      const b = deser.objects[1];
+      const c = deser.objects[2];
+      assert.ok(a instanceof g.Vertex);
+      assert.ok(b instanceof g.Edge);
+      assert.ok(c instanceof g.Vertex);
+
+      // Vertex properties default to [] when null/undefined
+      assert.ok(Array.isArray(a.properties));
+      assert.strictEqual(a.properties.length, 0);
+      assert.ok(Array.isArray(c.properties));
+      assert.strictEqual(c.properties.length, 0);
+
+      // Edge properties default to {} when null/undefined
+      assert.ok(b.properties);
+      assert.strictEqual(Object.keys(b.properties).length, 0);
+    });
+  });
+
 });
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js
index ed81ded7fd..913102b098 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js
@@ -129,12 +129,28 @@ describe('GraphSONReader', function () {
     assert.ok(result.objects);
     assert.ok(result.labels);
     assert.strictEqual(result.objects[2], 'lop');
-    assert.ok(result.objects[0] instanceof graph.Vertex);
-    assert.ok(result.objects[1] instanceof graph.Vertex);
-    assert.strictEqual(result.objects[0].label, 'person');
-    assert.strictEqual(result.objects[1].label, 'software');
+    const a = result.objects[0];
+    const bc = result.objects[1];
+    assert.ok(a instanceof graph.Vertex);
+    assert.ok(bc instanceof graph.Vertex);
+    assert.strictEqual(a.label, 'person');
+    assert.strictEqual(bc.label, 'software');
+    assert.ok(a.properties);
+    assert.ok(a.properties['name']);
+    assert.strictEqual(a.properties['name'].length, 1);
+    assert.strictEqual(a.properties['name'][0].value, 'marko');
+    assert.ok(a.properties['age']);
+    assert.strictEqual(a.properties['age'].length, 1);
+    assert.strictEqual(a.properties['age'][0].value, 29);
+    assert.ok(bc.properties);
+    assert.ok(bc.properties['name']);
+    assert.strictEqual(bc.properties['name'].length, 1);
+    assert.strictEqual(bc.properties['name'][0].value, 'lop');
+    assert.ok(bc.properties['lang']);
+    assert.strictEqual(bc.properties['lang'].length, 1);
+    assert.strictEqual(bc.properties['lang'][0].value, 'java');
   });
-  it('should parse paths from GraphSON3', function () {
+  it('should parse paths from GraphSON3 without properties', function () {
     const obj = {
       "@type" : "g:Path",
       "@value" : {
@@ -181,10 +197,14 @@ describe('GraphSONReader', function () {
     assert.ok(result.objects);
     assert.ok(result.labels);
     assert.strictEqual(result.objects[2], 'lop');
-    assert.ok(result.objects[0] instanceof graph.Vertex);
-    assert.ok(result.objects[1] instanceof graph.Vertex);
-    assert.strictEqual(result.objects[0].label, 'person');
-    assert.strictEqual(result.objects[1].label, 'software');
+    const a = result.objects[0];
+    const bc = result.objects[1];
+    assert.ok(a instanceof graph.Vertex);
+    assert.ok(bc instanceof graph.Vertex);
+    assert.strictEqual(a.label, 'person');
+    assert.strictEqual(bc.label, 'software');
+    assert.ok(a.properties === undefined);
+    assert.ok(bc.properties === undefined);
   });
 });
 describe('GraphSONWriter', function () {
diff --git a/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py b/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py
index a91f247475..346d5ffbb3 100644
--- a/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py
+++ b/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection.py
@@ -127,6 +127,21 @@ class TestDriverRemoteConnection(object):
         results = g.with_("materializeProperties", "tokens").V().properties().to_list()
         for vp in results:
             assert vp.properties is None or len(vp.properties) == 0
+        # #
+        # test materializeProperties in Path - GraphSON will deserialize into None and GraphBinary to []
+        p = g.with_("materializeProperties", "tokens").V().has('name', 'marko').outE().inV().has_label('software').path().next()
+        assert 3 == len(p.objects)
+        assert p.objects[0].properties is None or len(p.objects[0].properties) == 0
+        assert p.objects[1].properties is None or len(p.objects[1].properties) == 0
+        assert p.objects[2].properties is None or len(p.objects[2].properties) == 0
+        # #
+        # test materializeProperties in Path - 'all' should materialize properties on each element
+        p = g.with_("materializeProperties", "all").V().has('name', 'marko').outE().inV().has_label('software').path().next()
+        assert 3 == len(p.objects)
+        assert p.objects[0].properties is not None and len(p.objects[0].properties) > 0
+        # edges have dict-like properties; ensure not empty
+        assert p.objects[1].properties is not None and len(p.objects[1].properties) > 0
+        assert p.objects[2].properties is not None and len(p.objects[2].properties) > 0
 
     def test_lambda_traversals(self, remote_connection):
         statics.load_statics(globals())
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java
index 8636954ad5..b0eb6aedbe 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Context.java
@@ -18,6 +18,7 @@
  */
 package org.apache.tinkerpop.gremlin.server;
 
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
 import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.AbstractTraverser;
 import org.apache.tinkerpop.gremlin.structure.Element;
 import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceFactory;
@@ -327,8 +328,13 @@ public class Context {
             final Object firstElement = aggregate.get(0);
 
             if (firstElement instanceof Element) {
-                for (int i = 0; i < aggregate.size(); i++)
+                for (int i = 0; i < aggregate.size(); i++) {
                     aggregate.set(i, ReferenceFactory.detach((Element) aggregate.get(i)));
+                }
+            } else if (firstElement instanceof Path) {
+                for (int i = 0; i < aggregate.size(); i++) {
+                    aggregate.set(i, ReferenceFactory.detach((Path) aggregate.get(i)));
+                }
             } else if (firstElement instanceof AbstractTraverser) {
                 for (final Object item : aggregate)
                     ((AbstractTraverser) item).detach();
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerSerializationIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerSerializationIntegrateTest.java
index ebe6dfdf6c..cca1b80394 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerSerializationIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerSerializationIntegrateTest.java
@@ -22,11 +22,13 @@ import org.apache.tinkerpop.gremlin.driver.Client;
 import org.apache.tinkerpop.gremlin.driver.Cluster;
 import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
 import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.Property;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.structure.VertexProperty;
+import org.apache.tinkerpop.gremlin.util.Tokens;
 import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 import org.apache.tinkerpop.gremlin.util.ser.AbstractMessageSerializer;
 import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1;
@@ -42,7 +44,11 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 @RunWith(Parameterized.class)
 public class GremlinServerSerializationIntegrateTest extends AbstractGremlinServerIntegrationTest {
@@ -56,7 +62,7 @@ public class GremlinServerSerializationIntegrateTest extends AbstractGremlinServ
         this.serializer = serializer;
     }
 
-    @Parameterized.Parameters
+    @Parameterized.Parameters(name = "{0}")
     public static Collection serializers() {
         return Arrays.asList(new Object[][]{
                 {new GraphBinaryMessageSerializerV1()},
@@ -105,7 +111,7 @@ public class GremlinServerSerializationIntegrateTest extends AbstractGremlinServ
 
     @Test
     public void shouldSkipVertexPropertiesForBytecode() {
-        final Vertex vertex = g.with("materializeProperties", "tokens").V(1).next();
+        final Vertex vertex = g.with(Tokens.ARGS_MATERIALIZE_PROPERTIES, Tokens.MATERIALIZE_PROPERTIES_TOKENS).V(1).next();
 
         assertVertexWithoutProperties(vertex);
     }
@@ -133,7 +139,7 @@ public class GremlinServerSerializationIntegrateTest extends AbstractGremlinServ
 
     @Test
     public void shouldSkipEdgePropertiesForBytecode() {
-        final Edge edge = g.with("materializeProperties", "tokens").E(7).next();
+        final Edge edge = g.with(Tokens.ARGS_MATERIALIZE_PROPERTIES, Tokens.MATERIALIZE_PROPERTIES_TOKENS).E(7).next();
 
         assertEdgeWithoutProperties(edge);
     }
@@ -161,11 +167,36 @@ public class GremlinServerSerializationIntegrateTest extends AbstractGremlinServ
 
     @Test
     public void shouldSkipVertexPropertyPropertiesForBytecode() {
-        final Vertex vertex = g.with("materializeProperties", "tokens").V(7).next();
+        final Vertex vertex = g.with(Tokens.ARGS_MATERIALIZE_PROPERTIES, Tokens.MATERIALIZE_PROPERTIES_TOKENS).V(7).next();
 
         assertVertexWithoutVertexProperties(vertex);
     }
 
+    @Test
+    public void shouldDeserializePathPropertiesForScripts() {
+        final Path p = client.submit("gmodern.V().has('name','marko').outE().inV().hasLabel('software').path()").one().getPath();
+        assertPathElementsWithProperties(p);
+    }
+
+    @Test
+    public void shouldDeserializePathPropertiesForScriptsWithTokens() {
+        final Path p = client.submit("gmodern.with('materializeProperties','tokens').V().has('name','marko').outE().inV().hasLabel('software').path()").one().getPath();
+        assertPathElementsWithoutProperties(p);
+    }
+
+    @Test
+    public void shouldDeserializePathPropertiesForScriptsForBytecode() {
+        final Path p = g.V().has("name", "marko").outE().inV().hasLabel("software").path().next();
+        assertPathElementsWithProperties(p);
+    }
+
+    @Test
+    public void shouldDeserializePathPropertiesForScriptsWithTokensForBytecode() {
+        final Path p = g.with(Tokens.ARGS_MATERIALIZE_PROPERTIES, Tokens.MATERIALIZE_PROPERTIES_TOKENS).
+                V().has("name", "marko").outE().inV().hasLabel("software").path().next();
+        assertPathElementsWithoutProperties(p);
+    }
+
     // asserted vertex 7 from crew graph
     private void assertVertexWithVertexProperties(final Vertex vertex) {
         assertEquals(7, vertex.id());
@@ -224,4 +255,60 @@ public class GremlinServerSerializationIntegrateTest extends AbstractGremlinServ
         assertEquals(1, IteratorUtils.count(edge.properties()));
         assertEquals(0.5, edge.property("weight").value());
     }
+
+    private void assertPathElementsWithProperties(final Path p) {
+        // expect a V-E-V path
+        assertEquals(3, p.size());
+
+        final Object a = p.get(0);
+        final Object b = p.get(1);
+        final Object c = p.get(2);
+
+        // basic type assertions (prefer Hamcrest)
+        assertThat(a, instanceOf(Vertex.class));
+        assertThat(b, instanceOf(Edge.class));
+        assertThat(c, instanceOf(Vertex.class));
+
+        // properties should be present on each element in the path with expected key/values
+        final Vertex vOut = (Vertex) a;
+        final Edge e = (Edge) b;
+        final Vertex vIn = (Vertex) c;
+
+        // vertex 'a' should be marko with age 29
+        assertThat(IteratorUtils.count(vOut.properties()) > 0, is(true));
+        assertEquals("marko", vOut.value("name"));
+        assertEquals(Integer.valueOf(29), vOut.value("age"));
+
+        // edge 'b' should have weight 0.4
+        assertThat(IteratorUtils.count(e.properties()) > 0, is(true));
+        assertEquals(0.4, (Double) e.value("weight"), 0.0000001d);
+
+        // vertex 'c' should be lop with lang java
+        assertThat(IteratorUtils.count(vIn.properties()) > 0, is(true));
+        assertEquals("lop", vIn.value("name"));
+        assertEquals("java", vIn.value("lang"));
+    }
+
+    private void assertPathElementsWithoutProperties(final Path p) {
+        // expect a V-E-V path
+        assertEquals(3, p.size());
+
+        final Object a = p.get(0);
+        final Object b = p.get(1);
+        final Object c = p.get(2);
+
+        // basic type assertions (prefer Hamcrest)
+        assertThat(a, instanceOf(Vertex.class));
+        assertThat(b, instanceOf(Edge.class));
+        assertThat(c, instanceOf(Vertex.class));
+
+        // properties should NOT be present on each element in the path when materializeProperties is 'tokens'
+        final Vertex vOut = (Vertex) a;
+        final Edge e = (Edge) b;
+        final Vertex vIn = (Vertex) c;
+
+        assertEquals(0, IteratorUtils.count(vOut.properties()));
+        assertEquals(0, IteratorUtils.count(e.properties()));
+        assertEquals(0, IteratorUtils.count(vIn.properties()));
+    }
 }
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/SerializationTest.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/SerializationTest.java
index 9ddcf2321c..38c72092f6 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/SerializationTest.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/SerializationTest.java
@@ -53,6 +53,7 @@ import java.util.concurrent.TimeUnit;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
 
 import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -721,25 +722,21 @@ public class SerializationTest {
             final Vertex detachedVOut = detached.get("a");
             assertEquals(vOut.label(), detachedVOut.label());
             assertEquals(vOut.id(), detachedVOut.id());
-
-            // this is a SimpleTraverser so no properties are present in detachment
-            assertFalse(detachedVOut.properties().hasNext());
+            assertEquals("marko", detachedVOut.value("name").toString());
+            assertEquals(Integer.valueOf(29), detachedVOut.value("age"));
 
             final Edge e = p.get("b");
             final Edge detachedE = detached.get("b");
             assertEquals(e.label(), detachedE.label());
             assertEquals(e.id(), detachedE.id());
-
-            // this is a SimpleTraverser so no properties are present in detachment
-            assertFalse(detachedE.properties().hasNext());
+            assertEquals((double) e.value("weight"), detachedE.value("weight"), 0.0001d);
 
             final Vertex vIn = p.get("c");
             final Vertex detachedVIn = detached.get("c");
             assertEquals(vIn.label(), detachedVIn.label());
             assertEquals(vIn.id(), detachedVIn.id());
-
-            // this is a SimpleTraverser so no properties are present in detachment
-            assertFalse(detachedVIn.properties().hasNext());
+            assertEquals("lop", detachedVIn.value("name").toString());
+            assertEquals("java", detachedVIn.value("lang").toString());
         }
 
         @Test
@@ -923,25 +920,21 @@ public class SerializationTest {
             final Vertex detachedVOut = detached.get("a");
             assertEquals(vOut.label(), detachedVOut.label());
             assertEquals(vOut.id(), detachedVOut.id());
-
-            // this is a SimpleTraverser so no properties are present in detachment
-            assertFalse(detachedVOut.properties().hasNext());
+            assertEquals("marko", detachedVOut.value("name").toString());
+            assertEquals(Integer.valueOf(29), detachedVOut.value("age"));
 
             final Edge e = p.get("b");
             final Edge detachedE = detached.get("b");
             assertEquals(e.label(), detachedE.label());
             assertEquals(e.id(), detachedE.id());
-
-            // this is a SimpleTraverser so no properties are present in detachment
-            assertFalse(detachedE.properties().hasNext());
+            assertEquals((double) e.value("weight"), detachedE.value("weight"), 0.0001d);
 
             final Vertex vIn = p.get("c");
             final Vertex detachedVIn = detached.get("c");
             assertEquals(vIn.label(), detachedVIn.label());
             assertEquals(vIn.id(), detachedVIn.id());
-
-            // this is a SimpleTraverser so no properties are present in detachment
-            assertFalse(detachedVIn.properties().hasNext());
+            assertEquals("lop", detachedVIn.value("name").toString());
+            assertEquals("java", detachedVIn.value("lang").toString());
         }
 
         @Test
diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java
index 63918524ad..eaa3308bbc 100644
--- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java
+++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java
@@ -79,6 +79,7 @@ public final class Tokens {
     public static final String ARGS_HOST = "host";
     public static final String ARGS_SESSION = "session";
     public static final String ARGS_MANAGE_TRANSACTION = "manageTransaction";
+
     /**
      * The name of the argument that allows to control the serialization of properties on the server.
      */
@@ -88,8 +89,8 @@ public final class Tokens {
      * The name of the value denoting that all properties of Element should be returned.
      * Should be used with {@code ARGS_MATERIALIZE_PROPERTIES}
      */
-
     public static final String MATERIALIZE_PROPERTIES_ALL = "all";
+
     /**
      * The name of the value denoting that only `ID` and `Label` of Element should be returned.
      * Should be used with {@code ARGS_MATERIALIZE_PROPERTIES}
@@ -130,7 +131,7 @@ public final class Tokens {
     public static final String STATUS_ATTRIBUTE_STACK_TRACE = "stackTrace";
 
     /**
-     * A {@link ResultSet#statusAttributes()} key for user-facing warnings.
+     * A {@code ResultSet#statusAttributes()} key for user-facing warnings.
      * <p>
      * Implementations that set this key should consider using one of
      * these two recommended value types: