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

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

commit 99c11cfba77a9aa498ad64fafd6c80aae4c18bc2
Author: Yang Xia <[email protected]>
AuthorDate: Wed Aug 5 14:21:40 2026 -0700

    feat: add T.labels multi-label token
    
    Add the T.labels token as the token-level analogue of the labels() step and 
Element.labels(), operating on a vertex's full label set as a Set. It is 
accepted for projection (by), filtering (has, any-match), as an 
elementMap()/valueMap() map key, in the Gremlin grammar, across all GLVs, and 
on creation by addV(), mergeV(), and graph.addVertex() as a synonym for T.label.
    
    Under with("multilabel"), elementMap()/valueMap() now key the label set 
under T.labels rather than overloading T.label. Also fix addV() given an empty 
label set to create a label-less vertex on a ZERO_OR_MORE graph, consistent 
with graph.addVertex().
    
    TINKERPOP-3274
    
    Assisted-by: Kiro:claude-opus-4-8
---
 CHANGELOG.asciidoc                                 |   1 +
 docs/src/dev/provider/gremlin-semantics.asciidoc   |  27 +++---
 docs/src/reference/the-traversal.asciidoc          |  84 +++++++++++++++-
 docs/src/upgrade/release-4.x.x.asciidoc            |  35 ++++++-
 .../step/map/AbstractAddVertexStepPlaceholder.java |  18 ++++
 .../traversal/step/map/AddVertexStartStep.java     |   3 +-
 .../process/traversal/step/map/AddVertexStep.java  |   3 +-
 .../process/traversal/step/map/ElementMapStep.java |   4 +-
 .../traversal/step/map/MergeElementStep.java       |  40 +++++++-
 .../traversal/step/map/MergeVertexStep.java        |  19 ++--
 .../step/map/MergeVertexStepPlaceholder.java       |   2 +-
 .../traversal/step/map/PropertyMapStep.java        |   2 +-
 .../process/traversal/step/util/HasContainer.java  |   2 +-
 .../org/apache/tinkerpop/gremlin/structure/T.java  |  20 ++++
 .../gremlin/structure/util/ElementHelper.java      |  90 +++++++++++------
 .../gremlin/jsr223/ImportGremlinPluginTest.java    |  15 ++-
 .../traversal/lambda/TokenTraversalTest.java       |  20 ++++
 .../traversal/step/map/AddVertexStepTest.java      |  15 +++
 .../traversal/step/map/MergeEdgeStepTest.java      |  13 +++
 .../traversal/step/map/MergeVertexStepTest.java    |  30 ++++++
 .../apache/tinkerpop/gremlin/structure/TTest.java  |  15 +++
 .../src/Gremlin.Net/Process/Traversal/T.cs         |   3 +
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |   8 ++
 gremlin-go/driver/cucumber/cucumberSteps_test.go   |   2 +
 gremlin-go/driver/cucumber/gremlin.go              |   4 +
 gremlin-go/driver/traversal.go                     |  22 +++--
 .../gremlin-javascript/lib/process/traversal.ts    |   2 +-
 .../gremlin-javascript/test/cucumber/gremlin.js    |   4 +
 gremlin-language/src/main/antlr4/Gremlin.g4        |   2 +
 .../python/gremlin_python/process/traversal.py     |   3 +-
 .../src/main/python/tests/feature/gremlin.py       |   4 +
 .../gremlin/language/translator/translations.json  |  68 +++++++++++++
 .../gremlin/test/features/map/ElementMap.feature   |  28 +++---
 .../gremlin/test/features/map/Labels.feature       |  51 ++++++++++
 .../gremlin/test/features/map/ValueMap.feature     |  22 ++---
 .../step/sideEffect/LabelMutationPropertyTest.java |   2 +-
 .../step/sideEffect/LabelMutationStepTest.java     | 108 ++++++++++++++++++++-
 37 files changed, 676 insertions(+), 115 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 2ca2d637af..b2e7d6303b 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -65,6 +65,7 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Changed Tree class in Java to not extend from HashMap and offered a new 
tree-shaped API for navigation.
 * Added multi-label support for vertices with configurable `LabelCardinality` 
(`ONE`, `ONE_OR_MORE`, `ZERO_OR_MORE`).
 * Added `labels()` step to emit each vertex label as a separate traverser.
+* Added the `T.labels` token as the token-level analogue of `labels()` and 
`Element.labels()`, projecting and filtering on the full vertex label set, and 
accepted by `mergeV()`, `addV()`, and `graph.addVertex()` as a synonym for 
`T.label`.
 * Added `addLabel()` and `dropLabel()`/`dropLabels()` steps for vertex label 
mutation.
 * Added `LabelCardinalityValidator` utility for constraint enforcement 
separated from the `LabelCardinality` enum.
 * Added `with("multilabel")` source configuration for `elementMap()` and 
`valueMap()` to return labels as a set.
diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc 
b/docs/src/dev/provider/gremlin-semantics.asciidoc
index 5ba2fca94c..fc1e079a21 100644
--- a/docs/src/dev/provider/gremlin-semantics.asciidoc
+++ b/docs/src/dev/provider/gremlin-semantics.asciidoc
@@ -1746,9 +1746,8 @@ property entries specified by propertyKeys. If the list 
is empty, then all prope
 *Considerations:*
 
 The label format in the map is controlled by source-level `with("multilabel")` 
and `with("singlelabel")` options, not
-by the number of labels the graph permits per element. When `"multilabel"` is 
present, the label value is a
-`SET<STRING>`. Likewise when
-`"singlelabel"` is present, it is a single `STRING`. The two options are 
mutually exclusive. Configuring both
+by the number of labels the graph permits per element. When `"multilabel"` is 
present, the element's full label set is emitted under a `labels` key as a
+`SET<STRING>`. When `"singlelabel"` is present, a single `label` key is 
emitted as a `STRING`. The two options are mutually exclusive. Configuring both
 `"multilabel"` and `"singlelabel"` on the same source is rejected with a 
`State Error`. Note that it is a
 provider choice whether an unconfigured `GraphTraversal` defaults to 
`multilabel` or `singlelabel` semantics. The
 reference implementation defaults to `singlelabel` semantics.
@@ -1938,8 +1937,8 @@ 
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#groupcount-step[referenc
 *Arguments:*
 
 * `key` - The property key to check.
-* `accessor` - A `T` accessor: `T.id` (element identifier), `T.label` (element 
label), `T.key` (property key), or
-  `T.value` (property value).
+* `accessor` - A `T` accessor: `T.id` (element identifier), `T.label` (element 
label), `T.labels` (element
+  label set), `T.key` (property key), or `T.value` (property value).
 * `value` - The value to compare against using equality.
 * `predicate` - A `P` predicate for comparison. The predicate value may be a 
literal or a `Traversal`.
 * `traversal` - A child traversal whose first result is used as the comparison 
value (implicitly wrapped in `P.eq()`).
@@ -1955,6 +1954,8 @@ None
 
 * `has(key)` filters to elements that have the specified property (existence 
check).
 * `has(T.label, value)` is equivalent to `hasLabel(value)`. `has(T.id, value)` 
is equivalent to `hasId(value)`.
+* `has(T.labels, value)` is equivalent to `has(T.label, value)` and 
`hasLabel(value)`, matching when any of the
+  element's labels satisfies the comparison.
 * When the domain is a `Property` (e.g., from `properties()`), `T.key` and 
`T.value` accessors are used to filter
   by property key or property value respectively.
 * When the predicate value for `T.id` is a `STRING`, the element's 
`id().toString()` is used for comparison.
@@ -2587,9 +2588,11 @@ 
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#mergee-step[reference]
 * `onCreate` - A `MAP` used to specify additional existence criteria and/or 
properties not already specified in `searchCreate`.
 * `onMatch` - A `MAP` used to update the `Vertex` that is found using the 
`searchCreate` criteria.
 
-The `searchCreate` and `onCreate` `MAP` instances must consist of any 
combination of `T.id`, `T.label`, or arbitrary
-`STRING` keys (which are assumed to be vertex properties). The `onMatch` `MAP` 
instance only allows for `STRING` keys
-as the `id` and `label` of a `Vertex` are immutable. `null` values for these 
valid keys are not allowed.
+The `searchCreate` and `onCreate` `MAP` instances must consist of any 
combination of `T.id`, `T.label` (or its
+multi-label synonym `T.labels`), or arbitrary `STRING` keys (which are assumed 
to be vertex properties). The
+`onMatch` `MAP` instance allows `STRING` property keys, and on a graph that 
permits multiple labels per vertex it
+also accepts `T.label` or `T.labels` to add labels to the matched vertex. The 
`id` of a `Vertex` is immutable.
+`null` values for these valid keys are not allowed.
 
 The `MAP` that is used as the argument for `searchCreate` may be assigned from 
the incoming `Traverser` for the no-arg
 `mergeV()`. If `mergeV(Map)` is used, then it will override the incoming 
`Traverser`. If `mergeV(Traversal)` is used,
@@ -2623,7 +2626,8 @@ resolve to a `MAP`.
 *Exceptions:*
 
 * `MAP` arguments are validated for their keys resulting in exception if they 
do not meet requirements defined above.
-* Use of `T.label` should always have a value that is a `STRING`.
+* Use of `T.label` or `T.labels` should have a value that is a `STRING`, or a 
`SET<STRING>` on a graph that permits
+  multiple labels per vertex. `T.label` and `T.labels` may not both appear in 
the same `MAP`.
 * If `T.id` and/or `T.label` are specified in `searchCreate`, they cannot be 
overridden in `onCreate`.
 
 *Considerations:*
@@ -3322,9 +3326,8 @@ keys, the resulting map entry contains a `LIST` of the 
values. An `Edge` and a `
 value per key and the value is returned directly.
 
 The label format in the map is controlled by source-level `with("multilabel")` 
and `with("singlelabel")` options, not
-by the number of labels the graph permits per element. When `"multilabel"` is 
present, the label value is a
-`SET<STRING>`. Likewise when
-`"singlelabel"` is present, it is a single `STRING`. The two options are 
mutually exclusive. Configuring both
+by the number of labels the graph permits per element. When `"multilabel"` is 
present, the element's full label set is emitted under a `labels` key as a
+`SET<STRING>`. When `"singlelabel"` is present, a single `label` key is 
emitted as a `STRING`. The two options are mutually exclusive. Configuring both
 `"multilabel"` and `"singlelabel"` on the same source is rejected with a 
`State Error`. Note that it is a
 provider choice whether an unconfigured `GraphTraversal` defaults to 
`multilabel` or `singlelabel` semantics. The
 reference implementation defaults to `singlelabel` semantics.
diff --git a/docs/src/reference/the-traversal.asciidoc 
b/docs/src/reference/the-traversal.asciidoc
index 130ac5cda0..7bdfced48a 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -1168,8 +1168,8 @@ 
g.V().group().by(labels().fold()).by(values('name').fold())  <2>
 <1> `atlas` carries the "animal", "reptile", "aquatic", and "endangered" 
labels, but is grouped under only one of
 them, chosen non-deterministically, so this grouping is not reliable for 
multi-label vertices.
 <2> Grouping by the full set of labels instead keeps each animal under its own 
distinct combination of labels.
-Prefer this pattern with the <<labels-step,`labels()`>> step over `by(label)` 
for graphs that support multiple
-labels per vertex.
+Prefer this pattern with the <<labels-step,`labels()`>> step - or the 
equivalent <<t-labels-token,`by(T.labels)`>> -
+over `by(label)` for graphs that support multiple labels per vertex.
 
 *Additional References*
 
@@ -2912,6 +2912,86 @@ g.V().labels().count()
 
 
link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#labels()++[`labels()`]
 
+[llms-summary="The T.labels token is the token-level analogue of the labels() 
step and Element.labels(), projecting and filtering on a vertex's full label 
set as a Set<String>."]
+[[t-labels-token]]
+=== Labels Token
+
+The `T.labels` token (added in 4.0.0) is the token-level analogue of the 
<<labels-step,`labels()`>> step and
+`Element.labels()`. It operates on the full label set of an `Element`, 
projecting it as a `Set<String>`, and is valid
+wherever a `T` token is accepted: `by()`-modulation, `has()` filtering, as a 
map key, and as a creation token in
+<<addvertex-step,`addV()`>> and <<mergevertex-step,`mergeV()`>>. It 
complements the singular `T.label` token, which is
+backed by the deprecated <<label-step,`label()`>> step and returns only a 
single `String`. `T.labels` is the only `T`
+token whose value is a collection rather than a scalar, which shapes the 
caveats described below.
+
+NOTE: `by(T.labels)` and `has(T.labels, ...)` always operate on an element's 
complete label set, independent of any
+source-level `with("multilabel")` or `with("singlelabel")` configuration. That 
configuration governs only the map key
+emitted by <<elementmap-step,`elementMap()`>> and 
<<valuemap-step,`valueMap()`>>, which place the label set under a
+`labels` key under `multilabel` and a single `label` key otherwise. Code that 
reads a label from such a map must
+select the key that matches the active mode.
+
+Used for projection, `T.labels` returns the element's complete label set as a 
`Set<String>`:
+
+[gremlin-groovy,theZoo]
+----
+g.V().has('name','tux').project('labels').by(T.labels) <1>
+----
+
+<1> `tux` carries the "animal", "bird", "aquatic", and "endangered" labels, 
which are returned together as a
+`Set<String>`.
+
+Used for filtering, `has(T.labels, P)` matches an element when *any* label in 
its label set satisfies the predicate
+`P` (any-match / membership semantics), exactly as `hasLabel(...)` and the 
`labels()` step behave. This makes
+`has(T.labels, P)` equivalent to `has(T.label, P)`:
+
+[gremlin-groovy,theZoo]
+----
+g.V().has(T.labels, 'bird').values('name')                    <1>
+g.V().has(T.labels, within('bird','reptile')).values('name')  <2>
+----
+
+<1> Matches every vertex that carries the "bird" label.
+<2> Matches every vertex that carries either the "bird" or "reptile" label.
+
+The predicate is tested against each label independently, so `has(T.labels, 
P)` cannot express label-set equality.
+`has(T.labels, eq('bird'))` retains an element that carries `bird` alongside 
other labels, and a negation such as
+`has(T.labels, neq('bird'))` retains an element as long as *any* label differs 
from `bird`. This mirrors the
+multi-label behavior described for <<has-step,`hasLabel()`>>. Wrap the filter 
in <<not-step,`not()`>> to exclude every
+element carrying a label, or project with `by(T.labels)` and compare the 
resulting `Set` to test the label set as a
+whole.
+
+Because `T.labels` projects a `Set` rather than a scalar, steps that expect a 
single comparable value behave
+differently than they do with the other tokens:
+
+* `order().by(T.labels)` has *undefined ordering*. A `Set` has no natural 
total order, so the relative order of
+elements ordered this way is not guaranteed and should not be relied upon. 
When a deterministic order is needed,
+order by a scalar instead (for example `by(labels().order().fold())` or by a 
specific label).
+* `group().by(T.labels)` groups by *set-equality*: two elements fall into the 
same group only when their label sets
+are equal. This is the recommended way to group multi-label vertices, since 
`group().by(label)` collapses each
+vertex under a single, non-deterministically chosen label. The resulting map 
keys are `Set` instances.
+* Reductions and arithmetic that require a scalar, such as `sum()`, `mean()`, 
or `math()`, raise an error when applied
+to a `by(T.labels)` value.
+* `T.labels` is defined only on an `Element`. Applying it where the traverser 
is a `Property`, such as inside
+`properties()`, raises an `IllegalStateException`, the same as `T.label` and 
`T.id`.
+
+[gremlin-groovy,theZoo]
+----
+g.V().group().by(T.labels).by(values('name').fold()) <1>
+----
+
+<1> Vertices are grouped by their exact label set. Only vertices whose label 
sets are equal share a group.
+
+On creation, `T.labels` is accepted as a synonym for `T.label` by 
<<addvertex-step,`addV()`>>,
+<<mergevertex-step,`mergeV()`>>, and `graph.addVertex()`. The two tokens may 
not both appear in the same argument set.
+An empty or absent label set defers to the graph's `LabelCardinality`: a 
`ZERO_OR_MORE` graph creates a vertex with no
+labels, while `ONE` and `ONE_OR_MORE` apply the default label. This holds 
regardless of which of these APIs is used.
+
+NOTE: `T.labels` requires matched 4.x peers when sent over the wire. Older 
servers or drivers do not recognize the
+token. See the upgrade documentation for cross-version compatibility details.
+
+*Additional References*
+
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/T.html++[`T`]
+
 [llms-summary="The length()-step (map) returns the length incoming string or 
list of string traverser."]
 [[length-step]]
 === Length Step
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc 
b/docs/src/upgrade/release-4.x.x.asciidoc
index 3b270392ec..155732103b 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -142,10 +142,10 @@ gremlin> 
g.V().has('name','marko').dropLabel('employee').labels()
 ==>manager
 ----
 
-`mergeV()` now optionally accepts a list for `T.label` to match or create 
multi-label vertices. By default
-`elementMap()` and `valueMap()` still return a single label string, and a new 
traversal-source level
-`with("multilabel")` config alters the results to wrap labels in lists. The 
reference documentation covers each of these
-behaviors in detail.
+`mergeV()` and `addV()` accept the vertex label set via either `T.label` or 
the new `T.labels` token to match or
+create multi-label vertices. By default `elementMap()` and `valueMap()` return 
a single label string, while the
+traversal-source level `with("multilabel")` config returns the full label set. 
The reference documentation covers
+each of these behaviors in detail.
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3261[TINKERPOP-3261],
 
link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#tinkergraph-multi-label[Multi-Label],
@@ -157,6 +157,33 @@ 
link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#valuemap-step[val
 
link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#mergevertex-step[mergeV()],
 
link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#the-zoo-toy-graph[The
 Zoo]
 
+==== The `T.labels` Token
+
+The `T` enum gains a `labels` token as the token-level analogue of the 
`labels()` step and
+`Element.labels()`, completing the multi-label API. `T.labels` projects an 
element's full label set as a
+`Set<String>` and is accepted wherever the existing tokens are, including 
`by()`, `has()`, `mergeV()`,
+`addV()`, and as a map key. It complements `T.label`, which continues to 
produce the singular (now
+deprecated) label.
+
+[source,groovy]
+----
+gremlin> g.addV('person','employee').property('name','marko')
+==>v[0]
+gremlin> g.V().has('name','marko').project('labels').by(T.labels)
+==>[labels:[person,employee]]
+----
+
+Under `with("multilabel")`, `elementMap()` and `valueMap()` emit the label set 
under the `T.labels` key as a
+`Set<String>` rather than the `T.label` key. Code that reads the label set 
from the `T.label` key of a
+`multilabel` map must read it from `T.labels` instead. The default and 
`singlelabel` output is unchanged,
+emitting a single `String` under `T.label`. Because `T.labels` is a new token, 
traffic that carries it
+requires matched 4.x peers. An older peer that does not recognize the token 
raises an
+`IllegalArgumentException` on deserialization.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3274[TINKERPOP-3274],
+link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#elementmap-step[elementMap()],
+link:https://tinkerpop.apache.org/docs/4.0.0-beta.3/reference/#valuemap-step[valueMap()]
+
 ==== More Dynamic Arguments
 
 Prior to 4.0, comparing a traverser's value against a dynamically computed 
reference required several coordinated
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
index b48b53e9ff..69d7d04a4d 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AbstractAddVertexStepPlaceholder.java
@@ -22,6 +22,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValueHolder;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.Event;
+import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
 
 import java.util.Collection;
@@ -55,10 +56,27 @@ public abstract class AbstractAddVertexStepPlaceholder<S> 
extends AbstractAddEle
 
     @Override
     public void setLabel(Object label) {
+        // An explicit empty label collection means "no label provided": leave 
the label unset so vertex
+        // creation applies the graph's LabelCardinality default (no labels 
under ZERO_OR_MORE), matching the
+        // concrete step and graph.addVertex(). Flipping userProvidedLabel 
here would instead materialize the
+        // default label.
+        if (label instanceof Collection && ((Collection<?>) label).isEmpty()) {
+            return;
+        }
         super.setLabel(label);
         userProvidedLabel = true;
     }
 
+    @Override
+    public void addProperty(final Object key, final Object value) {
+        // T.labels is a synonym for T.label on vertex creation (multi-label 
support)
+        if (key == T.labels) {
+            setLabel(value);
+            return;
+        }
+        super.addProperty(key, value);
+    }
+
     @Override
     public boolean hasUserProvidedLabel() {
         return userProvidedLabel;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
index acb005f1f3..584e256881 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStartStep.java
@@ -128,7 +128,8 @@ public class AddVertexStartStep extends 
AbstractStep<Vertex, Vertex> implements
     }
 
     private void configureInternalParams(final Object... keyValues) {
-        if (keyValues[0] == T.label) {
+        // T.labels is accepted as a synonym for T.label on vertex creation 
(multi-label support)
+        if (keyValues[0] == T.label || keyValues[0] == T.labels) {
             setLabel(keyValues[1]);
             return;
         }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
index b2044a6f17..d73dc27acf 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStep.java
@@ -124,7 +124,8 @@ public class AddVertexStep<S> extends ScalarMapStep<S, 
Vertex> implements AddVer
     }
 
     private void configureInternalParams(final Object... keyValues) {
-        if (keyValues[0] == T.label) {
+        // T.labels is accepted as a synonym for T.label on vertex creation 
(multi-label support)
+        if (keyValues[0] == T.label || keyValues[0] == T.labels) {
             setLabel(keyValues[1]);
             return;
         }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
index 23befd72c9..33f50c49b0 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ElementMapStep.java
@@ -68,7 +68,7 @@ public class ElementMapStep<K,E> extends 
ScalarMapStep<Element, Map<K, E>> imple
             map.put(T.value, ((VertexProperty<?>) element).value());
         } else {
             if (isMultilabelEnabled()) {
-                map.put(T.label, element.labels());
+                map.put(T.labels, element.labels());
             } else {
                 final String label = element.label();
                 if (!label.isEmpty()) {
@@ -99,7 +99,7 @@ public class ElementMapStep<K,E> extends 
ScalarMapStep<Element, Map<K, E>> imple
         // can't add label if doing GraphComputer stuff as there is no access 
to the label of the adjacent vertex
         if (!onGraphComputer) {
             if (isMultilabelEnabled()) {
-                m.put(T.label, v.labels());
+                m.put(T.labels, v.labels());
             } else {
                 final String label = v.label();
                 if (!label.isEmpty()) m.put(T.label, label);
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java
index 6b7c261d48..19668f7167 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeElementStep.java
@@ -46,7 +46,6 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequire
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
 import org.apache.tinkerpop.gremlin.structure.Direction;
-import org.apache.tinkerpop.gremlin.structure.Element;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
@@ -249,13 +248,23 @@ public abstract class MergeElementStep<S, E, C> extends 
FlatMapStep<S, E>
     protected static void validate(final Map map, final boolean ignoreTokens, 
final Set allowedTokens, final String op) {
         if (null == map) return;
 
+        // T.labels is only a valid label token where the step allows it 
(mergeV, not mergeE); gate on allowedTokens
+        // so edge merges continue to reject it rather than silently treating 
it as a label.
+        final boolean labelsAllowed = allowedTokens.contains(T.labels);
+
+        // mergeV accepts T.labels as a synonym for T.label; specifying both 
in the same Map is ambiguous.
+        if (labelsAllowed && ((Map<?,?>) map).containsKey(T.label) && 
((Map<?,?>) map).containsKey(T.labels)) {
+            throw new IllegalArgumentException(String.format(
+                    "%s() does not allow both T.label and T.labels in the same 
Map", op));
+        }
+
         ((Map<?,?>) map).entrySet().forEach(e -> {
             final Object k = e.getKey();
             final Object v = e.getValue();
 
             if (ignoreTokens) {
-                // Allow T.label in onMatch for multi-label replacement support
-                if (k == T.label) {
+                // Allow T.label / T.labels in onMatch for multi-label 
replacement support
+                if (k == T.label || (labelsAllowed && k == T.labels)) {
                     if (v instanceof String) {
                         ElementHelper.validateLabel((String) v);
                     } else if (v instanceof java.util.Collection) {
@@ -288,7 +297,7 @@ public abstract class MergeElementStep<S, E, C> extends 
FlatMapStep<S, E>
                             "%s() and option(onCreate) args expect keys in Map 
to be either String or %s - check: %s",
                             op, allowedTokens, k));
                 }
-                if (k == T.label) {
+                if (k == T.label || (labelsAllowed && k == T.labels)) {
                     if (v instanceof String) {
                         ElementHelper.validateLabel((String) v);
                     } else if (v instanceof java.util.Collection) {
@@ -319,6 +328,29 @@ public abstract class MergeElementStep<S, E, C> extends 
FlatMapStep<S, E>
         });
     }
 
+    /**
+     * mergeV accepts the multi-label {@link T#labels} token as a synonym for 
{@link T#label}, matching the
+     * {@code elementMap()}/{@code valueMap()} output produced under {@code 
with("multilabel")}. Because the
+     * existence search, vertex creation, and onMatch label-application paths 
already accept a
+     * {@code Collection<String>} under {@link T#label}, any {@code T.labels} 
entry is normalized onto
+     * {@link T#label} at the input boundary, leaving the rest of the step 
label-token agnostic. The supplied
+     * Map is never mutated; a normalized copy is returned only when a {@code 
T.labels} entry is present.
+     *
+     * @throws IllegalArgumentException if the Map contains both {@code 
T.label} and {@code T.labels}
+     * @since 4.0.0
+     */
+    protected static Map normalizeLabelsToken(final Map map) {
+        if (map == null || !map.containsKey(T.labels))
+            return map;
+        // Enforce the same "not both" rule as validate()/getLabelsValue so 
this holds regardless of caller ordering.
+        if (map.containsKey(T.label)) {
+            throw new IllegalArgumentException("Vertex label may be specified 
with either T.label or T.labels, but not both");
+        }
+        final Map normalized = new LinkedHashMap(map);
+        normalized.put(T.label, normalized.remove(T.labels));
+        return normalized;
+    }
+
     /**
      * Prohibit overrides to the existence criteria (id/label/from/to) in 
onCreate.
      */
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
index 448ee768be..c972ec30b3 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStep.java
@@ -46,7 +46,7 @@ import static java.util.stream.Collectors.toList;
  */
 public class MergeVertexStep<S> extends MergeElementStep<S, Vertex, 
Map<Object, Object>> {
 
-    private static final Set allowedTokens = new 
LinkedHashSet(Arrays.asList(T.id, T.label));
+    private static final Set allowedTokens = new 
LinkedHashSet(Arrays.asList(T.id, T.label, T.labels));
 
     public static void validateMapInput(final Map map, final boolean 
ignoreTokens) {
         MergeElementStep.validate(map, ignoreTokens, allowedTokens, "mergeV");
@@ -78,8 +78,11 @@ public class MergeVertexStep<S> extends MergeElementStep<S, 
Vertex, Map<Object,
     protected Iterator<Vertex> flatMap(final Traverser.Admin<S> traverser) {
         final Graph graph = getGraph();
 
-        final Map mergeMap = materializeMap(traverser, mergeTraversal);
-        validateMapInput(mergeMap, false);
+        final Map rawMergeMap = materializeMap(traverser, mergeTraversal);
+        validateMapInput(rawMergeMap, false);
+        // T.labels is accepted as a synonym for T.label (multi-label 
support); normalize onto T.label so the
+        // downstream search/create/onMatch paths remain label-token agnostic.
+        final Map mergeMap = normalizeLabelsToken(rawMergeMap);
 
         Iterator<Vertex> vertices = searchVertices(mergeMap);
 
@@ -100,8 +103,10 @@ public class MergeVertexStep<S> extends 
MergeElementStep<S, Vertex, Map<Object,
                 traverser.set((S) v);
 
                 // assume good input from GraphTraversal - folks might drop in 
a T here even though it is immutable
-                final Map<Object, Object> onMatchMap = 
materializeMap(traverser, onMatchTraversal);
-                validateMapInput(onMatchMap, true);
+                final Map<Object, Object> rawOnMatchMap = 
materializeMap(traverser, onMatchTraversal);
+                validateMapInput(rawOnMatchMap, true);
+                // normalize T.labels onto T.label (multi-label synonym) 
before applying
+                final Map<Object, Object> onMatchMap = 
normalizeLabelsToken(rawOnMatchMap);
 
                 // Handle T.label separately: append-only addLabel semantics 
for multi-label support
                 Object labelValue = onMatchMap.get(T.label);
@@ -184,11 +189,13 @@ public class MergeVertexStep<S> extends 
MergeElementStep<S, Vertex, Map<Object,
         if (onCreateTraversal == null)
             return mergeMap;
 
-        final Map onCreateMap = materializeMap(traverser, onCreateTraversal);
+        Map onCreateMap = materializeMap(traverser, onCreateTraversal);
         // null result from onCreateTraversal - use main mergeMap argument
         if (onCreateMap == null || onCreateMap.size() == 0)
             return mergeMap;
         validateMapInput(onCreateMap, false);
+        // normalize T.labels onto T.label (multi-label synonym) to match the 
already-normalized mergeMap
+        onCreateMap = normalizeLabelsToken(onCreateMap);
 
         if (mergeMap == null || mergeMap.size() == 0)
             return onCreateMap;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java
index fa14904514..55f9699572 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepPlaceholder.java
@@ -55,7 +55,7 @@ import static 
org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEleme
  */
 public class MergeVertexStepPlaceholder<S> extends 
AbstractMergeElementStepPlaceholder<S, Vertex> {
 
-    private static final Set allowedTokens = new 
LinkedHashSet(Arrays.asList(T.id, T.label));
+    private static final Set allowedTokens = new 
LinkedHashSet(Arrays.asList(T.id, T.label, T.labels));
 
     public static void validateMapInput(final Map map, final boolean 
ignoreTokens) {
         MergeElementStep.validate(map, ignoreTokens, allowedTokens, "mergeV");
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
index a773d87b07..f3a515a80d 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/PropertyMapStep.java
@@ -204,7 +204,7 @@ public class PropertyMapStep<K,E> extends 
ScalarMapStep<Element, Map<K, E>>
             } else {
                 if (includeToken(WithOptions.labels)) {
                     if (isMultilabelEnabled()) {
-                        map.put(T.label, element.labels());
+                        map.put(T.labels, element.labels());
                     } else {
                         final String label = getElementLabel(element);
                         if (!label.isEmpty()) {
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
index a94b2e3881..3b171bd522 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/util/HasContainer.java
@@ -55,7 +55,7 @@ public class HasContainer implements Serializable, Cloneable, 
Predicate<Element>
         if (this.key != null) {
             if (this.key.equals(T.id.getAccessor()))
                 return testingIdString ? testIdAsString(element) : 
testId(element);
-            if (this.key.equals(T.label.getAccessor()))
+            if (this.key.equals(T.label.getAccessor()) || 
this.key.equals(T.labels.getAccessor()))
                 return testLabel(element);
         }
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java
index 66f6822f2c..9f3ed7b21f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/T.java
@@ -18,6 +18,7 @@
  */
 package org.apache.tinkerpop.gremlin.structure;
 
+import java.util.Set;
 import java.util.function.Function;
 
 /**
@@ -44,6 +45,22 @@ public enum T implements Function<Element, Object> {
             return element.label();
         }
     },
+    /**
+     * Labels (representing Element.labels())
+     *
+     * @since 4.0.0
+     */
+    labels {
+        @Override
+        public String getAccessor() {
+            return LABELS;
+        }
+
+        @Override
+        public Set<String> apply(final Element element) {
+            return element.labels();
+        }
+    },
     /**
      * Id (representing Element.id())
      *
@@ -94,6 +111,7 @@ public enum T implements Function<Element, Object> {
     };
 
     private static final String LABEL = Graph.Hidden.hide("label");
+    private static final String LABELS = Graph.Hidden.hide("labels");
     private static final String ID = Graph.Hidden.hide("id");
     private static final String KEY = Graph.Hidden.hide("key");
     private static final String VALUE = Graph.Hidden.hide("value");
@@ -106,6 +124,8 @@ public enum T implements Function<Element, Object> {
     public static T fromString(final String accessor) {
         if (accessor.equals(LABEL))
             return label;
+        else if (accessor.equals(LABELS))
+            return labels;
         else if (accessor.equals(ID))
             return id;
         else if (accessor.equals(KEY))
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
index 0ca8aeda13..e17a4b7e83 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
@@ -238,48 +238,74 @@ public final class ElementHelper {
      * @throws NullPointerException if the value for the {@link T#label} key 
is {@code null}
      */
     public static Optional<String> getLabelValue(final Object... keyValues) {
-        for (int i = 0; i < keyValues.length; i = i + 2) {
-            if (keyValues[i].equals(T.label)) {
-                ElementHelper.validateLabel((String) keyValues[i + 1]);
-                return Optional.of((String) keyValues[i + 1]);
-            }
-        }
-        return Optional.empty();
+        final Object labelValue = resolveLabelTokenValue(keyValues);
+        if (labelValue == LABEL_TOKEN_ABSENT)
+            return Optional.empty();
+        // Cast directly so a non-String value yields ClassCastException, per 
this method's contract.
+        ElementHelper.validateLabel((String) labelValue);
+        return Optional.of((String) labelValue);
     }
 
     /**
      * Extracts the value of the {@link T#label} key from the list of 
arguments as a {@link Set} of labels.
      * Supports both single {@link String} values and {@link 
java.util.Collection} values for multi-label vertices.
+     * The multi-label synonym {@link T#labels} is also accepted; specifying 
both tokens is rejected.
      *
      * @param keyValues a list of key/value pairs
-     * @return the labels associated with {@link T#label}, or empty if not 
present
+     * @return the labels associated with {@link T#label}/{@link T#labels}, or 
empty if not present
      * @since 4.0.0
      */
     public static Optional<Set<String>> getLabelsValue(final Object... 
keyValues) {
-        for (int i = 0; i < keyValues.length; i = i + 2) {
-            if (keyValues[i].equals(T.label)) {
-                final Object labelValue = keyValues[i + 1];
-                if (labelValue instanceof String) {
-                    ElementHelper.validateLabel((String) labelValue);
-                    final Set<String> labels = new LinkedHashSet<>();
-                    labels.add((String) labelValue);
-                    return Optional.of(labels);
-                } else if (labelValue instanceof Collection) {
-                    final Set<String> labels = new LinkedHashSet<>();
-                    for (final Object l : (Collection<?>) labelValue) {
-                        if (!(l instanceof String)) {
-                            throw new IllegalArgumentException("T.label 
collection must contain only Strings");
-                        }
-                        ElementHelper.validateLabel((String) l);
-                        labels.add((String) l);
-                    }
-                    return Optional.of(labels);
-                } else {
-                    throw new IllegalArgumentException("T.label value must be 
String or Collection<String>");
+        final Object labelValue = resolveLabelTokenValue(keyValues);
+        if (labelValue == LABEL_TOKEN_ABSENT)
+            return Optional.empty();
+        if (labelValue instanceof String) {
+            ElementHelper.validateLabel((String) labelValue);
+            final Set<String> labels = new LinkedHashSet<>();
+            labels.add((String) labelValue);
+            return Optional.of(labels);
+        } else if (labelValue instanceof Collection) {
+            final Set<String> labels = new LinkedHashSet<>();
+            for (final Object l : (Collection<?>) labelValue) {
+                if (!(l instanceof String)) {
+                    throw new IllegalArgumentException("T.label/T.labels 
collection must contain only Strings");
                 }
+                ElementHelper.validateLabel((String) l);
+                labels.add((String) l);
             }
+            return Optional.of(labels);
+        } else {
+            throw new IllegalArgumentException("T.label/T.labels value must be 
String or Collection<String>");
         }
-        return Optional.empty();
+    }
+
+    private static final Object LABEL_TOKEN_ABSENT = new Object();
+
+    /**
+     * Resolves the label value from a key/value array, accepting either 
{@link T#label} or its multi-label
+     * synonym {@link T#labels} (the first occurrence of a given token wins). 
Returns {@link #LABEL_TOKEN_ABSENT}
+     * when neither token is present.
+     *
+     * @throws IllegalArgumentException if both {@code T.label} and {@code 
T.labels} are supplied
+     */
+    private static Object resolveLabelTokenValue(final Object... keyValues) {
+        boolean sawLabel = false, sawLabels = false;
+        Object labelValue = LABEL_TOKEN_ABSENT;
+        for (int i = 0; i < keyValues.length; i = i + 2) {
+            final Object k = keyValues[i];
+            if (k.equals(T.label)) {
+                if (sawLabels)
+                    throw new IllegalArgumentException("Vertex label may be 
specified with either T.label or T.labels, but not both");
+                if (!sawLabel) labelValue = keyValues[i + 1];
+                sawLabel = true;
+            } else if (k.equals(T.labels)) {
+                if (sawLabel)
+                    throw new IllegalArgumentException("Vertex label may be 
specified with either T.label or T.labels, but not both");
+                if (!sawLabels) labelValue = keyValues[i + 1];
+                sawLabels = true;
+            }
+        }
+        return labelValue;
     }
 
     /**
@@ -323,7 +349,7 @@ public final class ElementHelper {
                 
element.graph().features().vertex().properties().supportsNullPropertyValues();
 
         for (int i = 0; i < propertyKeyValues.length; i = i + 2) {
-            if (!propertyKeyValues[i].equals(T.id) && 
!propertyKeyValues[i].equals(T.label))
+            if (!propertyKeyValues[i].equals(T.id) && 
!propertyKeyValues[i].equals(T.label) && !propertyKeyValues[i].equals(T.labels))
                 if (!allowNullPropertyValues && null == propertyKeyValues[i + 
1])
                     element.properties(((String) 
propertyKeyValues[i])).forEachRemaining(Property::remove);
                 else
@@ -348,7 +374,7 @@ public final class ElementHelper {
         final boolean allowNullPropertyValues = 
vertex.graph().features().vertex().supportsNullPropertyValues();
 
         for (int i = 0; i < propertyKeyValues.length; i = i + 2) {
-            if (!propertyKeyValues[i].equals(T.id) && 
!propertyKeyValues[i].equals(T.label))
+            if (!propertyKeyValues[i].equals(T.id) && 
!propertyKeyValues[i].equals(T.label) && !propertyKeyValues[i].equals(T.labels))
                 if (!allowNullPropertyValues && null == propertyKeyValues[i + 
1])
                     vertex.properties(((String) 
propertyKeyValues[i])).forEachRemaining(VertexProperty::remove);
                 else
@@ -374,7 +400,7 @@ public final class ElementHelper {
         final boolean allowNullPropertyValues = 
vertex.graph().features().vertex().supportsNullPropertyValues();
 
         for (int i = 0; i < propertyKeyValues.length; i = i + 2) {
-            if (!propertyKeyValues[i].equals(T.id) && 
!propertyKeyValues[i].equals(T.label))
+            if (!propertyKeyValues[i].equals(T.id) && 
!propertyKeyValues[i].equals(T.label) && !propertyKeyValues[i].equals(T.labels))
                 if (!allowNullPropertyValues && null == propertyKeyValues[i + 
1])
                     vertex.properties(((String) 
propertyKeyValues[i])).forEachRemaining(VertexProperty::remove);
                 else
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java
index c628f48578..f0732e1264 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinPluginTest.java
@@ -18,12 +18,6 @@
  */
 package org.apache.tinkerpop.gremlin.jsr223;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.hasItems;
-import static org.hamcrest.Matchers.is;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.Arrays;
@@ -36,6 +30,11 @@ import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.io.IoCore;
 import org.apache.tinkerpop.gremlin.util.Gremlin;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 import org.junit.Test;
 
 /**
@@ -117,8 +116,8 @@ public class ImportGremlinPluginTest {
 
         final DefaultImportCustomizer customizer = (DefaultImportCustomizer) 
module.getCustomizers().get()[0];
         assertEquals(1, module.getCustomizers().get().length);
-        assertThat(customizer.getEnumImports(), hasItems(T.id, T.key, T.label, 
T.value));
-        assertEquals(4, customizer.getEnumImports().size());
+        assertThat(customizer.getEnumImports(), hasItems(T.id, T.key, T.label, 
T.labels, T.value));
+        assertEquals(5, customizer.getEnumImports().size());
     }
 
     @Test
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java
index 6e3588cbbe..d078cb6b85 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/lambda/TokenTraversalTest.java
@@ -26,6 +26,9 @@ import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.structure.VertexProperty;
 import org.junit.Test;
 
+import java.util.Collections;
+import java.util.Set;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -40,6 +43,23 @@ public class TokenTraversalTest {
         assertEquals(100, t.next().intValue());
     }
 
+    @Test
+    public void shouldWorkOnVertexWithLabels() {
+        final TokenTraversal<Vertex, Set<String>> t = new 
TokenTraversal<>(T.labels);
+        final Vertex v = mock(Vertex.class);
+        when(v.labels()).thenReturn(Collections.singleton("person"));
+        t.addStart(new B_O_Traverser<>(v, 1).asAdmin());
+        assertEquals(Collections.singleton("person"), t.next());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void shouldThrowOnPropertyWithLabels() {
+        // T.labels is defined only on an Element; applying it to a Property 
is unsupported, like T.label/T.id
+        final TokenTraversal<Property<String>, Set<String>> t = new 
TokenTraversal<>(T.labels);
+        final Property<String> pr = mock(Property.class);
+        t.addStart(new B_O_Traverser<>(pr, 1).asAdmin());
+    }
+
     @Test
     public void shouldWorkOnVertexProperty() {
         final TokenTraversal<VertexProperty, Integer> t = new 
TokenTraversal<>(T.id);
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java
index 583e1cb845..d176e48986 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexStepTest.java
@@ -468,4 +468,19 @@ public class AddVertexStepTest extends GValueStepTest {
     private GraphTraversal.Admin<Object, Vertex> 
getMultiLabelGValueTraversal() {
         return __.addV(GValue.of("l1", "person"), GValue.of("l2", 
"employee")).asAdmin();
     }
+
+    @Test
+    public void shouldRouteLabelsTokenPropertyWithGValueToLabel() {
+        // property(T.labels, <GValue>) must route to the label (not be stored 
as a property) and
+        // resolve/pin the GValue exactly as the T.label path does, confirming 
GValue symmetry.
+        final GraphTraversal.Admin<Object, Vertex> traversal =
+                __.addV().property(T.labels, GValue.of("l1", Set.of("person", 
"employee"))).asAdmin();
+        final AddVertexStepPlaceholder<?> step = (AddVertexStepPlaceholder<?>) 
traversal.getSteps().get(0);
+        final Object resolved = step.getLabel();
+        assertTrue(resolved instanceof Set);
+        assertEquals(Set.of("person", "employee"), resolved);
+        assertFalse(step.getProperties().containsKey(T.labels));
+        assertFalse(step.getProperties().containsKey(T.label));
+        verifyVariables(traversal, Set.of("l1"), Set.of());
+    }
 }
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java
index 1f143fe8f0..f0e9946dac 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeEdgeStepTest.java
@@ -160,6 +160,19 @@ public class MergeEdgeStepTest extends GValueStepTest {
         MergeEdgeStep.validateMapInput(m, true);
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldNotAllowLabelsTokenForMergeE() {
+        final Map<Object,Object> m = CollectionUtil.asMap(T.labels, 
Set.of("knows"));
+        MergeEdgeStep.validateMapInput(m, false);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldNotAllowLabelsTokenForMergeEOnMatch() {
+        // edges are single-label; T.labels must be rejected on the onMatch 
(ignoreTokens) path too
+        final Map<Object,Object> m = CollectionUtil.asMap(T.labels, 
Set.of("knows"));
+        MergeEdgeStep.validateMapInput(m, true);
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void shouldFailToValidateWithoutTokens() {
         final Map<Object,Object> m = CollectionUtil.asMap("k", "v",
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java
index 71a12cc815..65d519add7 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/MergeVertexStepTest.java
@@ -127,6 +127,36 @@ public class MergeVertexStepTest extends GValueStepTest {
         MergeVertexStep.validateMapInput(m, false);
     }
 
+    @Test
+    public void shouldValidateWithLabelsToken() {
+        final Map<Object,Object> m = CollectionUtil.asMap("k", "v",
+                T.labels, Set.of("person", "employee"),
+                T.id, 10000);
+        MergeVertexStep.validateMapInput(m, false);
+    }
+
+    @Test
+    public void shouldValidateLabelsTokenOnMatch() {
+        final Map<Object,Object> m = CollectionUtil.asMap(T.labels, 
Set.of("person"));
+        MergeVertexStep.validateMapInput(m, true);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldNotAllowBothLabelAndLabels() {
+        final Map<Object,Object> m = CollectionUtil.asMap(
+                T.label, "person",
+                T.labels, Set.of("employee"));
+        MergeVertexStep.validateMapInput(m, false);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldNotAllowBothLabelAndLabelsOnMatch() {
+        final Map<Object,Object> m = CollectionUtil.asMap(
+                T.label, "person",
+                T.labels, Set.of("employee"));
+        MergeVertexStep.validateMapInput(m, true);
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void shouldFailToValidateWithTokensBecauseOfValue() {
         final Map<Object,Object> m = CollectionUtil.asMap("k", "v",
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java
index 18c30172c8..443f0d01cc 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/TTest.java
@@ -20,6 +20,8 @@ package org.apache.tinkerpop.gremlin.structure;
 
 import org.junit.Test;
 
+import java.util.Collections;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -33,6 +35,11 @@ public class TTest {
         assertEquals(T.label, T.fromString(T.label.getAccessor()));
     }
 
+    @Test
+    public void shouldGetLabelsEnumFromString() {
+        assertEquals(T.labels, T.fromString(T.labels.getAccessor()));
+    }
+
     @Test
     public void shouldGetIdEnumFromString() {
         assertEquals(T.id, T.fromString(T.id.getAccessor()));
@@ -61,6 +68,14 @@ public class TTest {
         assertEquals("knows", T.label.apply(e));
     }
 
+    @Test
+    public void shouldApplyLabelsOnElement() {
+        final Element e = mock(Element.class);
+        when(e.labels()).thenReturn(Collections.singleton("knows"));
+
+        assertEquals(Collections.singleton("knows"), T.labels.apply(e));
+    }
+
     @Test
     public void shouldApplyIdOnElement() {
         final Element e = mock(Element.class);
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs 
b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs
index deb477eeac..5148b5a76d 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/T.cs
@@ -41,6 +41,8 @@ namespace Gremlin.Net.Process.Traversal
 
         public static T Label => new T("label");
 
+        public static T Labels => new T("labels");
+
         public static T Value => new T("value");
 
         private static readonly IDictionary<string, T> Properties = new 
Dictionary<string, T>
@@ -48,6 +50,7 @@ namespace Gremlin.Net.Process.Traversal
             { "id", Id },
             { "key", Key },
             { "label", Label },
+            { "labels", Labels },
             { "value", Value },
         };
 
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index ecb968a108..214a721f29 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -1401,6 +1401,10 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_V_hasLabelXpersonX_labels_single_label_graph", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().HasLabel("person").Labels()}}, 
                {"g_V_labels_single_label_graph", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Labels()}}, 
                {"g_E_labels_single_label_graph", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.E().HasLabel("knows").Labels()}}, 
+               {"g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Has("name", "marko").Project<object>("labels").By(T.Labels)}}, 
+               {"g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Has("name", "tux").Project<object>("labels").By(T.Labels)}}, 
+               {"g_V_hasXT_labels_personX_valuesXnameX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Has(T.Labels, "person").Values<object>("name")}}, 
+               
{"g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.With("multilabel").V().Has(T.Labels, P.Within("bird", 
"reptile")).Values<object>("name")}}, 
                {"g_injectXfeature_test_nullX_length", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>("feature", "test", null).Length()}}, 
                {"g_injectXfeature_test_nullX_lengthXlocalX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>("feature", "test", 
null).Length<object>(Scope.Local)}}, 
                {"g_injectXListXa_bXX_length", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(new List<object> { "a", "b" }).Length()}}, 
@@ -3759,6 +3763,10 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_V_hasLabelXpersonX_labels_single_label_graph", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().HasLabel("person").Labels()}}, 
                {"g_V_labels_single_label_graph", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Labels()}}, 
                {"g_E_labels_single_label_graph", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.E().HasLabel("knows").Labels()}}, 
+               {"g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Has("name", "marko").Project<object>("labels").By(T.Labels)}}, 
+               {"g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Has("name", "tux").Project<object>("labels").By(T.Labels)}}, 
+               {"g_V_hasXT_labels_personX_valuesXnameX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V().Has(T.Labels, "person").Values<object>("name")}}, 
+               
{"g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.With("multilabel").V().Has(T.Labels, P.Within("bird", 
"reptile")).Values<object>("name")}}, 
                {"g_injectXfeature_test_nullX_length", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>("feature", "test", null).Length()}}, 
                {"g_injectXfeature_test_nullX_lengthXlocalX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>("feature", "test", 
null).Length<object>(Scope.Local)}}, 
                {"g_injectXListXa_bXX_length", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(new List<object> { "a", "b" }).Length()}}, 
diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go 
b/gremlin-go/driver/cucumber/cucumberSteps_test.go
index 6089a47523..99c5873793 100644
--- a/gremlin-go/driver/cucumber/cucumberSteps_test.go
+++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go
@@ -401,6 +401,8 @@ func toT(name, graphName string) interface{} {
        // Return as is, since T values are just strings.
        if name == "label" {
                return gremlingo.T.Label
+       } else if name == "labels" {
+               return gremlingo.T.Labels
        } else if name == "id" {
                return gremlingo.T.Id
        } else if name == "key" {
diff --git a/gremlin-go/driver/cucumber/gremlin.go 
b/gremlin-go/driver/cucumber/gremlin.go
index e95627b537..a6fdeef710 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -1354,6 +1354,10 @@ var translationMap = map[string][]func(g 
*gremlingo.GraphTraversalSource, p map[
     "g_V_hasLabelXpersonX_labels_single_label_graph": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().HasLabel("person").Labels()}}, 
     "g_V_labels_single_label_graph": {func(g *gremlingo.GraphTraversalSource, 
p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Labels()}}, 
     "g_E_labels_single_label_graph": {func(g *gremlingo.GraphTraversalSource, 
p map[string]interface{}) *gremlingo.GraphTraversal {return 
g.E().HasLabel("knows").Labels()}}, 
+    "g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().Has("name", 
"marko").Project("labels").By(gremlingo.T.Labels)}}, 
+    "g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().Has("name", 
"tux").Project("labels").By(gremlingo.T.Labels)}}, 
+    "g_V_hasXT_labels_personX_valuesXnameX": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().Has(gremlingo.T.Labels, 
"person").Values("name")}}, 
+    "g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX": 
{func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return 
g.With("multilabel").V().Has(gremlingo.T.Labels, gremlingo.P.Within("bird", 
"reptile")).Values("name")}}, 
     "g_injectXfeature_test_nullX_length": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.Inject("feature", "test", nil).Length()}}, 
     "g_injectXfeature_test_nullX_lengthXlocalX": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.Inject("feature", "test", 
nil).Length(gremlingo.Scope.Local)}}, 
     "g_injectXListXa_bXX_length": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Inject([]interface{}{"a", "b"}).Length()}}, 
diff --git a/gremlin-go/driver/traversal.go b/gremlin-go/driver/traversal.go
index a78f6ee9c2..82434f5d52 100644
--- a/gremlin-go/driver/traversal.go
+++ b/gremlin-go/driver/traversal.go
@@ -387,20 +387,22 @@ var Scope = scopes{
 type t string
 
 type ts struct {
-       Id    t
-       Label t
-       Id_   t
-       Key   t
-       Value t
+       Id     t
+       Label  t
+       Labels t
+       Id_    t
+       Key    t
+       Value  t
 }
 
 // T is string symbols.
 var T = ts{
-       Id:    "id",
-       Label: "label",
-       Id_:   "id_",
-       Key:   "key",
-       Value: "value",
+       Id:     "id",
+       Label:  "label",
+       Labels: "labels",
+       Id_:    "id_",
+       Key:    "key",
+       Value:  "value",
 }
 
 type materializeProperties struct {
diff --git a/gremlin-js/gremlin-javascript/lib/process/traversal.ts 
b/gremlin-js/gremlin-javascript/lib/process/traversal.ts
index a8f3abfc19..188e22ea79 100644
--- a/gremlin-js/gremlin-javascript/lib/process/traversal.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/traversal.ts
@@ -616,5 +616,5 @@ export const order = toEnum('Order', 'asc desc shuffle');
 export const pick = toEnum('Pick', 'any none unproductive');
 export const pop = toEnum('Pop', 'all first last mixed');
 export const scope = toEnum('Scope', 'global local');
-export const t = toEnum('T', 'id key label value');
+export const t = toEnum('T', 'id key label labels value');
 export const n = toEnum('N', 'byte_ short_ int_ long_ float_ double_ bigInt 
bigDecimal');
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js 
b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index 69d1632c39..b9af73f9b7 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -1385,6 +1385,10 @@ const gremlins = {
     g_V_hasLabelXpersonX_labels_single_label_graph: [function({g}) { return 
g.V().hasLabel("person").labels() }], 
     g_V_labels_single_label_graph: [function({g}) { return g.V().labels() }], 
     g_E_labels_single_label_graph: [function({g}) { return 
g.E().hasLabel("knows").labels() }], 
+    g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX: [function({g}) { return 
g.V().has("name", "marko").project("labels").by(T.labels) }], 
+    g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX: [function({g}) { return 
g.V().has("name", "tux").project("labels").by(T.labels) }], 
+    g_V_hasXT_labels_personX_valuesXnameX: [function({g}) { return 
g.V().has(T.labels, "person").values("name") }], 
+    g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX: 
[function({g}) { return g.with_("multilabel").V().has(T.labels, 
P.within("bird", "reptile")).values("name") }], 
     g_injectXfeature_test_nullX_length: [function({g}) { return 
g.inject("feature", "test", null).length() }], 
     g_injectXfeature_test_nullX_lengthXlocalX: [function({g}) { return 
g.inject("feature", "test", null).length(Scope.local) }], 
     g_injectXListXa_bXX_length: [function({g}) { return g.inject(["a", 
"b"]).length() }], 
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 
b/gremlin-language/src/main/antlr4/Gremlin.g4
index d15a2982df..5c7c0547ad 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -1024,6 +1024,7 @@ traversalT
 traversalTShort
     : K_ID
     | K_LABEL
+    | K_LABELS
     | K_KEY
     | K_VALUE
     ;
@@ -1031,6 +1032,7 @@ traversalTShort
 traversalTLong
     : K_T DOT K_ID
     | K_T DOT K_LABEL
+    | K_T DOT K_LABELS
     | K_T DOT K_KEY
     | K_T DOT K_VALUE
     ;
diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py 
b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index a030ac616c..28772da343 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
@@ -244,10 +244,11 @@ Scope = Enum('Scope', ' global_ local')
 statics.add_static('global_', Scope.global_)
 statics.add_static('local', Scope.local)
 
-T = Enum('T', ' id id_ key label value')
+T = Enum('T', ' id id_ key label labels value')
 
 statics.add_static('id', T.id)
 statics.add_static('label', T.label)
+statics.add_static('labels', T.labels)
 statics.add_static('id_', T.id_)
 statics.add_static('key', T.key)
 statics.add_static('value', T.value)
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py 
b/gremlin-python/src/main/python/tests/feature/gremlin.py
index fc5b396346..5de5b49a67 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -1359,6 +1359,10 @@ world.gremlins = {
     'g_V_hasLabelXpersonX_labels_single_label_graph': [(lambda 
g:g.V().has_label('person').labels())], 
     'g_V_labels_single_label_graph': [(lambda g:g.V().labels())], 
     'g_E_labels_single_label_graph': [(lambda 
g:g.E().has_label('knows').labels())], 
+    'g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX': [(lambda 
g:g.V().has('name', 'marko').project('labels').by(T.labels))], 
+    'g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX': [(lambda 
g:g.V().has('name', 'tux').project('labels').by(T.labels))], 
+    'g_V_hasXT_labels_personX_valuesXnameX': [(lambda g:g.V().has(T.labels, 
'person').values('name'))], 
+    'g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX': 
[(lambda g:g.with_('multilabel').V().has(T.labels, P.within('bird', 
'reptile')).values('name'))], 
     'g_injectXfeature_test_nullX_length': [(lambda g:g.inject('feature', 
'test', None).length())], 
     'g_injectXfeature_test_nullX_lengthXlocalX': [(lambda 
g:g.inject('feature', 'test', None).length(Scope.local))], 
     'g_injectXListXa_bXX_length': [(lambda g:g.inject(['a', 'b']).length())], 
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
index efd55f89ad..ae18159e02 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
@@ -26890,6 +26890,74 @@
             }
         ]
     },
+    {
+        "scenario": "g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX",
+        "traversals": [
+            {
+                "original": "g.V().has(\"name\", 
\"marko\").project(\"labels\").by(T.labels)",
+                "language": "g.V().has(\"name\", 
\"marko\").project(\"labels\").by(T.labels)",
+                "canonical": "g.V().has(\"name\", 
\"marko\").project(\"labels\").by(T.labels)",
+                "anonymized": "g.V().has(string0, 
string1).project(string2).by(T.labels)",
+                "dotnet": "g.V().Has(\"name\", 
\"marko\").Project<object>(\"labels\").By(T.Labels)",
+                "go": "g.V().Has(\"name\", 
\"marko\").Project(\"labels\").By(gremlingo.T.Labels)",
+                "groovy": "g.V().has(\"name\", 
\"marko\").project(\"labels\").by(T.labels)",
+                "java": "g.V().has(\"name\", 
\"marko\").project(\"labels\").by(T.labels)",
+                "javascript": "g.V().has(\"name\", 
\"marko\").project(\"labels\").by(T.labels)",
+                "python": "g.V().has('name', 
'marko').project('labels').by(T.labels)"
+            }
+        ]
+    },
+    {
+        "scenario": "g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX",
+        "traversals": [
+            {
+                "original": "g.V().has(\"name\", 
\"tux\").project(\"labels\").by(T.labels)",
+                "language": "g.V().has(\"name\", 
\"tux\").project(\"labels\").by(T.labels)",
+                "canonical": "g.V().has(\"name\", 
\"tux\").project(\"labels\").by(T.labels)",
+                "anonymized": "g.V().has(string0, 
string1).project(string2).by(T.labels)",
+                "dotnet": "g.V().Has(\"name\", 
\"tux\").Project<object>(\"labels\").By(T.Labels)",
+                "go": "g.V().Has(\"name\", 
\"tux\").Project(\"labels\").By(gremlingo.T.Labels)",
+                "groovy": "g.V().has(\"name\", 
\"tux\").project(\"labels\").by(T.labels)",
+                "java": "g.V().has(\"name\", 
\"tux\").project(\"labels\").by(T.labels)",
+                "javascript": "g.V().has(\"name\", 
\"tux\").project(\"labels\").by(T.labels)",
+                "python": "g.V().has('name', 
'tux').project('labels').by(T.labels)"
+            }
+        ]
+    },
+    {
+        "scenario": "g_V_hasXT_labels_personX_valuesXnameX",
+        "traversals": [
+            {
+                "original": "g.V().has(T.labels, \"person\").values(\"name\")",
+                "language": "g.V().has(T.labels, \"person\").values(\"name\")",
+                "canonical": "g.V().has(T.labels, 
\"person\").values(\"name\")",
+                "anonymized": "g.V().has(T.labels, string0).values(string1)",
+                "dotnet": "g.V().Has(T.Labels, 
\"person\").Values<object>(\"name\")",
+                "go": "g.V().Has(gremlingo.T.Labels, 
\"person\").Values(\"name\")",
+                "groovy": "g.V().has(T.labels, \"person\").values(\"name\")",
+                "java": "g.V().has(T.labels, \"person\").values(\"name\")",
+                "javascript": "g.V().has(T.labels, 
\"person\").values(\"name\")",
+                "python": "g.V().has(T.labels, 'person').values('name')"
+            }
+        ]
+    },
+    {
+        "scenario": 
"g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX",
+        "traversals": [
+            {
+                "original": "g.with(\"multilabel\").V().has(T.labels, 
P.within(\"bird\", \"reptile\")).values(\"name\")",
+                "language": "g.with(\"multilabel\").V().has(T.labels, 
P.within(\"bird\", \"reptile\")).values(\"name\")",
+                "canonical": "g.with(\"multilabel\").V().has(T.labels, 
P.within(\"bird\", \"reptile\")).values(\"name\")",
+                "anonymized": "g.with(string0).V().has(T.labels, 
P.within(string1, string2)).values(string3)",
+                "dotnet": "g.With(\"multilabel\").V().Has(T.Labels, 
P.Within(\"bird\", \"reptile\")).Values<object>(\"name\")",
+                "go": "g.With(\"multilabel\").V().Has(gremlingo.T.Labels, 
gremlingo.P.Within(\"bird\", \"reptile\")).Values(\"name\")",
+                "groovy": "g.with(\"multilabel\").V().has(T.labels, 
P.within(\"bird\", \"reptile\")).values(\"name\")",
+                "java": "g.with(\"multilabel\").V().has(T.labels, 
P.within(\"bird\", \"reptile\")).values(\"name\")",
+                "javascript": "g.with_(\"multilabel\").V().has(T.labels, 
P.within(\"bird\", \"reptile\")).values(\"name\")",
+                "python": "g.with_('multilabel').V().has(T.labels, 
P.within('bird', 'reptile')).values('name')"
+            }
+        ]
+    },
     {
         "scenario": "g_injectXfeature_test_nullX_length",
         "traversals": [
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
index ea7e30297b..4acb2559fd 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
@@ -61,12 +61,12 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": "marko", 
"age": 29}] |
-      | m[{"t[id]": "v[josh].id", "t[label]": "s[person]", "name": "josh", 
"age": 32}] |
-      | m[{"t[id]": "v[peter].id", "t[label]": "s[person]", "name": "peter", 
"age": 35}] |
-      | m[{"t[id]": "v[vadas].id", "t[label]": "s[person]", "name": "vadas", 
"age": 27}] |
-      | m[{"t[id]": "v[lop].id", "t[label]": "s[software]", "name": "lop", 
"lang": "java"}] |
-      | m[{"t[id]": "v[ripple].id", "t[label]": "s[software]", "name": 
"ripple", "lang": "java"}] |
+      | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": "marko", 
"age": 29}] |
+      | m[{"t[id]": "v[josh].id", "t[labels]": "s[person]", "name": "josh", 
"age": 32}] |
+      | m[{"t[id]": "v[peter].id", "t[labels]": "s[person]", "name": "peter", 
"age": 35}] |
+      | m[{"t[id]": "v[vadas].id", "t[labels]": "s[person]", "name": "vadas", 
"age": 27}] |
+      | m[{"t[id]": "v[lop].id", "t[labels]": "s[software]", "name": "lop", 
"lang": "java"}] |
+      | m[{"t[id]": "v[ripple].id", "t[labels]": "s[software]", "name": 
"ripple", "lang": "java"}] |
 
   Scenario: g_V_elementMapXname_ageX
     Given the modern graph
@@ -122,7 +122,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": "marko", 
"age": 29}] |
+      | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": "marko", 
"age": 29}] |
 
   @MultiLabel @SingleLabelDefault
   Scenario: g_V_elementMap_single_label_default
@@ -152,7 +152,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[tux].id", "t[label]": 
"s[animal,bird,aquatic,endangered]", "name": "tux", "species": "african 
penguin"}] |
+      | m[{"t[id]": "v[tux].id", "t[labels]": 
"s[animal,bird,aquatic,endangered]", "name": "tux", "species": "african 
penguin"}] |
 
   @MultiLabel @MultiLabelDefault
   Scenario: g_V_elementMap_multi_label_default
@@ -168,7 +168,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": 
"marko"}] |
+      | m[{"t[id]": "v[marko].id", "t[labels]": "s[person,employee]", "name": 
"marko"}] |
 
   @MultiLabel @MultiLabelDefault
   Scenario: g_withXsinglelabelX_V_elementMap_multi_label_default
@@ -198,7 +198,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": "marko"}] |
+      | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": "marko"}] 
|
 
   @MultiLabel
   Scenario: g_withXsinglelabelX_V_elementMap_zero_label_vertex
@@ -230,7 +230,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": "nobody"}] |
+      | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": "nobody"}] |
 
   @MultiLabel @MultiLabelDefault
   Scenario: g_V_elementMap_zero_label_vertex_multi_label_default
@@ -246,7 +246,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": "nobody"}] |
+      | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": "nobody"}] |
 
   @MultiLabel @SingleLabelDefault
   Scenario: g_V_elementMap_zero_label_vertex_single_label_default
@@ -274,7 +274,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "e[atlas-livesIn->lagoon].id", "t[label]": "s[livesIn]", 
"since": 2018, "D[OUT]": "m[{\\"t[id]\\": \\"v[atlas].id\\", \\"t[label]\\": 
\\"s[animal,reptile,aquatic,endangered]\\"}]", "D[IN]": "m[{\\"t[id]\\": 
\\"v[lagoon].id\\", \\"t[label]\\": \\"s[habitat,aquatic]\\"}]"}] |
+      | m[{"t[id]": "e[atlas-livesIn->lagoon].id", "t[labels]": "s[livesIn]", 
"since": 2018, "D[OUT]": "m[{\\"t[id]\\": \\"v[atlas].id\\", \\"t[labels]\\": 
\\"s[animal,reptile,aquatic,endangered]\\"}]", "D[IN]": "m[{\\"t[id]\\": 
\\"v[lagoon].id\\", \\"t[labels]\\": \\"s[habitat,aquatic]\\"}]"}] |
 
   @GraphComputerVerificationReferenceOnly @MultiLabel @MultiLabelDefault
   Scenario: g_E_elementMap_multi_label_default
@@ -290,7 +290,7 @@ Feature: Step - elementMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "e[marko-knows->josh].id", "t[label]": "s[knows]", 
"weight": "d[0.5].d", "D[OUT]": "m[{\\"t[id]\\": \\"v[marko].id\\", 
\\"t[label]\\": \\"s[person]\\"}]", "D[IN]": "m[{\\"t[id]\\": \\"v[josh].id\\", 
\\"t[label]\\": \\"s[person]\\"}]"}] |
+      | m[{"t[id]": "e[marko-knows->josh].id", "t[labels]": "s[knows]", 
"weight": "d[0.5].d", "D[OUT]": "m[{\\"t[id]\\": \\"v[marko].id\\", 
\\"t[labels]\\": \\"s[person]\\"}]", "D[IN]": "m[{\\"t[id]\\": 
\\"v[josh].id\\", \\"t[labels]\\": \\"s[person]\\"}]"}] |
 
   @GraphComputerVerificationReferenceOnly @MultiLabel @SingleLabelDefault
   Scenario: g_E_elementMap_single_label_default
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
index 26fb1db50d..fc9a356449 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Labels.feature
@@ -132,3 +132,54 @@ Feature: Step - labels()
       | result |
       | knows |
       | knows |
+
+  Scenario: g_V_hasXname_markoX_projectXlabelsX_byXT_labelsX
+    Given the modern graph
+    And the traversal of
+      """
+      g.V().has("name", "marko").project("labels").by(T.labels)
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | m[{"labels": "s[person]"}] |
+
+  @MultiLabel
+  Scenario: g_V_hasXname_tuxX_projectXlabelsX_byXT_labelsX
+    Given the zoo graph
+    And the traversal of
+      """
+      g.V().has("name", "tux").project("labels").by(T.labels)
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | m[{"labels": "s[animal,bird,aquatic,endangered]"}] |
+
+  Scenario: g_V_hasXT_labels_personX_valuesXnameX
+    Given the modern graph
+    And the traversal of
+      """
+      g.V().has(T.labels, "person").values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | marko |
+      | vadas |
+      | josh |
+      | peter |
+
+  @MultiLabel
+  Scenario: 
g_withXmultilabelX_V_hasXT_labels_withinXbird_reptileXX_valuesXnameX
+    Given the zoo graph
+    And the traversal of
+      """
+      g.with("multilabel").V().has(T.labels, P.within("bird", 
"reptile")).values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | tux |
+      | atlas |
+      | monty |
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
index 791d6333e8..a2762a94f6 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
@@ -93,12 +93,12 @@ Feature: Step - valueMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": ["marko"], 
"age": [29]}] |
-      | m[{"t[id]": "v[josh].id", "t[label]": "s[person]", "name": ["josh"], 
"age": [32]}] |
-      | m[{"t[id]": "v[peter].id", "t[label]": "s[person]", "name": ["peter"], 
"age": [35]}] |
-      | m[{"t[id]": "v[vadas].id", "t[label]": "s[person]", "name": ["vadas"], 
"age": [27]}] |
-      | m[{"t[id]": "v[lop].id", "t[label]": "s[software]", "name": ["lop"], 
"lang": ["java"]}] |
-      | m[{"t[id]": "v[ripple].id", "t[label]": "s[software]", "name": 
["ripple"], "lang": ["java"]}] |
+      | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": 
["marko"], "age": [29]}] |
+      | m[{"t[id]": "v[josh].id", "t[labels]": "s[person]", "name": ["josh"], 
"age": [32]}] |
+      | m[{"t[id]": "v[peter].id", "t[labels]": "s[person]", "name": 
["peter"], "age": [35]}] |
+      | m[{"t[id]": "v[vadas].id", "t[labels]": "s[person]", "name": 
["vadas"], "age": [27]}] |
+      | m[{"t[id]": "v[lop].id", "t[labels]": "s[software]", "name": ["lop"], 
"lang": ["java"]}] |
+      | m[{"t[id]": "v[ripple].id", "t[labels]": "s[software]", "name": 
["ripple"], "lang": ["java"]}] |
 
   Scenario: g_V_valueMapXname_ageX
     Given the modern graph
@@ -264,7 +264,7 @@ Feature: Step - valueMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[marko].id", "t[label]": "s[person]", "name": ["marko"], 
"age": [29]}] |
+      | m[{"t[id]": "v[marko].id", "t[labels]": "s[person]", "name": 
["marko"], "age": [29]}] |
 
   @MultiLabel @SingleLabelDefault
   Scenario: g_V_valueMap_withXtokensX_single_label_default
@@ -294,7 +294,7 @@ Feature: Step - valueMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[tux].id", "t[label]": 
"s[animal,bird,aquatic,endangered]", "name": ["tux"], "species": ["african 
penguin"]}] |
+      | m[{"t[id]": "v[tux].id", "t[labels]": 
"s[animal,bird,aquatic,endangered]", "name": ["tux"], "species": ["african 
penguin"]}] |
 
   @MultiLabel @MultiLabelDefault
   Scenario: g_V_valueMap_withXtokensX_multi_label_default
@@ -310,7 +310,7 @@ Feature: Step - valueMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[marko].id", "t[label]": "s[person,employee]", "name": 
["marko"]}] |
+      | m[{"t[id]": "v[marko].id", "t[labels]": "s[person,employee]", "name": 
["marko"]}] |
 
   @MultiLabel @MultiLabelDefault
   Scenario: g_withXsinglelabelX_V_valueMap_withXtokensX_multi_label_default
@@ -356,7 +356,7 @@ Feature: Step - valueMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": ["nobody"]}] |
+      | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": ["nobody"]}] |
 
   @MultiLabel @MultiLabelDefault
   Scenario: g_V_valueMapXtrueX_zero_label_vertex_multi_label_default
@@ -372,7 +372,7 @@ Feature: Step - valueMap()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"t[id]": "v[nobody].id", "t[label]": "s[]", "name": ["nobody"]}] |
+      | m[{"t[id]": "v[nobody].id", "t[labels]": "s[]", "name": ["nobody"]}] |
 
   @MultiLabel @SingleLabelDefault
   Scenario: g_V_valueMapXtrueX_zero_label_vertex_single_label_default
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
index b9bc0e4d41..90571ba693 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationPropertyTest.java
@@ -190,7 +190,7 @@ public class LabelMutationPropertyTest {
             // with multilabel config: should return Set<String>
             final GraphTraversalSource gml = g.with("multilabel");
             final Map<Object, Object> mapWithConfig = 
gml.V(v).valueMap(true).next();
-            final Object labelWithConfig = mapWithConfig.get(T.label);
+            final Object labelWithConfig = mapWithConfig.get(T.labels);
             assertThat("Iteration " + i + ": with multilabel config, label 
should be a Set",
                     labelWithConfig, instanceOf(Set.class));
             assertThat("Iteration " + i + ": with multilabel config, labels 
should match",
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
index 761d714524..a12d6c8d12 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/process/traversal/step/sideEffect/LabelMutationStepTest.java
@@ -18,6 +18,7 @@
  */
 package 
org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.step.sideEffect;
 
+import org.apache.tinkerpop.gremlin.process.traversal.Merge;
 import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions;
 import org.apache.tinkerpop.gremlin.structure.Edge;
@@ -30,6 +31,9 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -184,7 +188,7 @@ public class LabelMutationStepTest {
         final Vertex v = g.addV("person").addLabel("employee").next();
         final GraphTraversalSource gml = g.with("multilabel");
         final Map<Object, Object> map = gml.V(v).valueMap(true).next();
-        final Object labelValue = map.get(T.label);
+        final Object labelValue = map.get(T.labels);
         assertThat(labelValue, instanceOf(Set.class));
         final Set<String> labels = (Set<String>) labelValue;
         assertThat(labels, containsInAnyOrder("person", "employee"));
@@ -227,12 +231,112 @@ public class LabelMutationStepTest {
         final Vertex v = g.addV("person").addLabel("employee").next();
         final GraphTraversalSource gml = g.with("multilabel");
         final Map<Object, Object> map = gml.V(v).elementMap().next();
-        final Object labelValue = map.get(T.label);
+        final Object labelValue = map.get(T.labels);
         assertThat(labelValue, instanceOf(Set.class));
         final Set<String> labels = (Set<String>) labelValue;
         assertThat(labels, containsInAnyOrder("person", "employee"));
     }
 
+    // --- mergeV T.labels symmetry tests ---
+
+    @Test
+    public void shouldCreateMultiLabelVertexWithMergeVUsingLabelsToken() {
+        final Map<Object, Object> merge = new LinkedHashMap<>();
+        merge.put(T.labels, new LinkedHashSet<>(Arrays.asList("person", 
"employee")));
+        final Vertex v = g.mergeV(merge).next();
+        assertThat(v.labels(), containsInAnyOrder("person", "employee"));
+    }
+
+    @Test
+    public void shouldMatchExistingVertexByLabelsToken() {
+        final Vertex v = g.addV("person").addLabel("employee").next();
+        final Map<Object, Object> merge = new LinkedHashMap<>();
+        merge.put(T.labels, new LinkedHashSet<>(Arrays.asList("person", 
"employee")));
+        // T.labels normalizes to T.label and drives an AND label match, 
resolving to the existing vertex
+        final Vertex merged = g.mergeV(merge).next();
+        assertThat(merged.id(), is(v.id()));
+        assertThat(g.V().count().next(), is(1L));
+    }
+
+    @Test
+    public void shouldMatchExistingVertexWhenMergeVFedMultilabelElementMap() {
+        final Vertex v = 
g.addV("person").addLabel("employee").property("name", "marko").next();
+        final GraphTraversalSource gml = g.with("multilabel");
+        // multilabel elementMap keys the label set under T.labels; feeding it 
straight back into
+        // mergeV must resolve to the same vertex (T.labels treated as a 
synonym for T.label).
+        final Map<Object, Object> elementMap = gml.V(v).elementMap().next();
+        final Vertex merged = g.mergeV(elementMap).next();
+        assertThat(merged.id(), is(v.id()));
+        assertThat(merged.labels(), containsInAnyOrder("person", "employee"));
+    }
+
+    @Test
+    public void shouldApplyLabelsTokenOnMatch() {
+        final Vertex v = g.addV("person").property("name", "marko").next();
+        final Map<Object, Object> search = new LinkedHashMap<>();
+        search.put(T.label, "person");
+        final Map<Object, Object> onMatch = new LinkedHashMap<>();
+        onMatch.put(T.labels, new LinkedHashSet<>(Arrays.asList("employee", 
"manager")));
+        g.mergeV(search).option(Merge.onMatch, onMatch).iterate();
+        assertThat(v.labels(), containsInAnyOrder("person", "employee", 
"manager"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldRejectBothLabelAndLabelsInMergeV() {
+        final Map<Object, Object> merge = new LinkedHashMap<>();
+        merge.put(T.label, "person");
+        merge.put(T.labels, new LinkedHashSet<>(Arrays.asList("employee")));
+        g.mergeV(merge).iterate();
+    }
+
+    // --- addV / addVertex T.labels create symmetry tests ---
+
+    @Test
+    public void shouldCreateVertexWithLabelsTokenViaAddV() {
+        final Vertex v = g.addV().property(T.labels, new 
LinkedHashSet<>(Arrays.asList("person", "employee"))).next();
+        assertThat(v.labels(), containsInAnyOrder("person", "employee"));
+    }
+
+    @Test
+    public void shouldCreateVertexWithLabelsTokenViaGraphAddVertex() {
+        final Vertex v = graph.addVertex(T.labels, new 
LinkedHashSet<>(Arrays.asList("person", "employee")), "name", "marko");
+        assertThat(v.labels(), containsInAnyOrder("person", "employee"));
+        // T.labels must drive the label set, not be attached as a property
+        assertThat(v.value("name"), is("marko"));
+    }
+
+    @Test
+    public void shouldCreateZeroLabelVertexWithEmptyLabelsToken() {
+        // graph is ZERO_OR_MORE, so an empty label set is permitted on create
+        final Vertex v = graph.addVertex(T.labels, new 
LinkedHashSet<String>());
+        assertThat(v.labels(), hasSize(0));
+    }
+
+    @Test
+    public void shouldCreateZeroLabelVertexWithEmptyLabelsTokenViaAddV() {
+        // an explicit empty T.labels set on addV() behaves like 
graph.addVertex(T.labels, []) under ZERO_OR_MORE
+        final Vertex v = g.addV().property(T.labels, new 
LinkedHashSet<String>()).next();
+        assertThat(v.labels(), hasSize(0));
+    }
+
+    @Test
+    public void shouldCreateZeroLabelVertexWithEmptyLabelTokenViaAddV() {
+        // the same holds for an explicit empty collection under the singular 
T.label token
+        final Vertex v = g.addV().property(T.label, new 
LinkedHashSet<String>()).next();
+        assertThat(v.labels(), hasSize(0));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldRejectBothLabelAndLabelsOnGraphAddVertex() {
+        graph.addVertex(T.label, "person", T.labels, new 
LinkedHashSet<>(Arrays.asList("employee")));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldRejectLabelSetTwiceViaAddVArgAndLabelsProperty() {
+        // supplying a label via the addV() argument and again via T.labels is 
rejected (label already set)
+        g.addV("person").property(T.labels, new 
LinkedHashSet<>(Arrays.asList("employee"))).iterate();
+    }
+
     // --- GraphTraversalSource multi-label addV test ---
 
     @Test

Reply via email to