This is an automated email from the ASF dual-hosted git repository. kenhuuu pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 2722cef70f25ecb60d5d263a36a53473f4f36bf6 Author: Ken Hu <[email protected]> AuthorDate: Tue Mar 31 00:23:27 2026 -0700 Update GraphSON4 specification and fix mistakes in Java implementation CTR --- docs/src/dev/io/graphson.asciidoc | 59 +++++++++++++++++----- .../structure/io/graphson/GraphSONMapper.java | 7 ++- .../structure/io/graphson/GraphSONModule.java | 3 ++ .../io/graphson/GraphSONSerializersV4.java | 41 +++++++++++++++ .../io/graphson/GraphSONTypeSerializerV4.java | 36 ------------- .../graphson/GraphSONMapperEmbeddedTypeTest.java | 14 +++++ 6 files changed, 109 insertions(+), 51 deletions(-) diff --git a/docs/src/dev/io/graphson.asciidoc b/docs/src/dev/io/graphson.asciidoc index f9cbafff9d..7b3a872d9b 100644 --- a/docs/src/dev/io/graphson.asciidoc +++ b/docs/src/dev/io/graphson.asciidoc @@ -27,6 +27,9 @@ Server where arbitrary objects of varying types may be returned as results. Howe is only intended to be a network serialization format that is only able to serialize specific types defined by the format. It is only meant to be used with the TinkerPop HTTP API. +NOTE: This document focuses on GraphSON 4. For the GraphSON 1, 2, and 3 specifications, please refer to the TinkerPop +3.x IO Documentation. + When considering GraphSON as a "graph" format, the relevant feature to consider is the `writeGraph` and `readGraph` methods on the `GraphSONWriter` and `GraphSONReader` interfaces, respectively. These methods write the entire `Graph` instance as output or read an entire `Graph` instance input and they do so in a way external to generalized object @@ -110,6 +113,10 @@ Version 4.0 of GraphSON was first introduced on TinkerPop 4.0.0 and is represent many underused or duplicated types have been removed, labels are now list of strings and request/response formats have changed quite a bit, and custom types have been replaced with Provider Defined Type (PDT). +For each type below, two examples are provided. The first shows the typed representation +(`application/vnd.gremlin-v4.0+json`) and the second shows the untyped representation +(`application/vnd.gremlin-v4.0+json;types=false`). + === Boolean Matches the JSON Boolean and doesn't have type information. @@ -424,13 +431,13 @@ JSON String form of UUID. === Edge -JSON Object (required keys are: id, label, inVLabel, outVLabel, inV, outV) + +JSON Object (required keys are: id, label, inV, outV) + "id" is any GraphSON 4.0 type + -"inV", "outV" is an Object that contains "id" which is any GraphSON 4.0 type and "label" which is a `g:List` of `String` -"label" is a `g:List` of `String` + +"inV", "outV" is an Object that contains "id" which is any GraphSON 4.0 type and "label" which is a JSON Array of `String` +"label" is a JSON Array of `String` + "properties" is an optional Object containing Arrays of `g:Property` -The untyped version has one additional required key "type" which is always "vertex". +The untyped version has one additional required key "type" which is always "edge". [source,json] ---- @@ -2302,8 +2309,8 @@ JSON Object with two required keys: "key" and "value" + === Tree -JSON Object with one or more possibly nested "key" "value" pairs -"key" is an Element (`g:Vertex`, `g:Edge`, `g:VertexProperty`) +JSON Array containing zero or more possibly nested JSON Objects, each containing a "key" and "value" pair + +"key" is an Element (`g:Vertex`, `g:Edge`, `g:VertexProperty`) + "value" is a `g:Tree` making this a recursively defined structure [source,json] @@ -2431,12 +2438,13 @@ JSON Object with one or more possibly nested "key" "value" pairs === Vertex -JSON Object with required keys: "id", "label", "properties" + +JSON Object with required keys: "id", "label" + "id" is any GraphSON 4.0 type + -"label" is a `g:List` of `String` + +"label" is a JSON Array of `String` + "properties" is an optional Object containing Arrays of `g:VertexProperty` -The untyped version has one additional required key "type" which is always "vertex". +The untyped version has one additional required key "type" which is always "vertex". Also note the difference in how +labels are written. More details can be found in the `VertexProperty` section. [source,json] ---- @@ -2615,11 +2623,17 @@ The untyped version has one additional required key "type" which is always "vert === VertexProperty -JSON Object with required keys: "id", "value", "label", "properties" + +JSON Object with required keys: "id", "value", "label" + "id" is any type GraphSON 4.0 type + "value" is any type GraphSON 4.0 type + -"label" is a `g:List` of `String` + -"properties" is an optional Object containing Arrays of "g:Property" (metaproperties) +"label" is a JSON Array of `String` + +"properties" is an optional Object for metaproperties where each key is a meta-property name and each value is any +GraphSON 4.0 type. + +NOTE: When a `VertexProperty` is serialized as a standalone typed value (i.e. wrapped in a `g:VertexProperty` envelope), +the "label" field is always included. However, when vertex properties are serialized as part of a `Vertex` in untyped +mode, the "label" is omitted because the properties are already grouped under their property key name in the containing +object, making the label redundant. [source,json] ---- @@ -2706,6 +2720,23 @@ JSON String of the enum value. "label" ---- +=== Merge + +JSON String of the enum value. + +[source,json] +---- +{ + "@type" : "g:Merge", + "@value" : "onCreate" +} +---- + +[source,text] +---- +"onCreate" +---- + === Standard Request The following `RequestMessage` is an example of a simple sessionless request for a script evaluation with parameters. @@ -2950,7 +2981,7 @@ The following `ResponseMessage` is a typical example of the typical successful r === Error Result -The following `ResponseMessage` is a typical example of the typical successful response Gremlin Server will return when returning results from a script. +The following `ResponseMessage` is a typical example of an error response the Gremlin Server will return from a script. [source,json] ---- @@ -2983,7 +3014,7 @@ The following `ResponseMessage` is a typical example of the typical successful r } ---- -Note that the "extended" types require the addition of the separate `GraphSONXModuleV4d0` module as follows: +Note that the "extended" types require the addition of the separate `GraphSONXModuleV4` module as follows: [source,java] ---- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java index 68abfac42b..3da5c4e367 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java @@ -106,7 +106,12 @@ public class GraphSONMapper implements Mapper<ObjectMapper> { .typeProperty(GraphSONTokens.VALUETYPE); // Registers native Java types that are supported by Jackson - registerJavaBaseTypes(graphSONTypeIdResolver); + if (version == GraphSONVersion.V4_0) { + graphSONTypeIdResolver.addCustomType( + String.format("%s:%s", GraphSONTokens.GREMLIN_TYPE_NAMESPACE, UUID.class.getSimpleName()), UUID.class); + } else { + registerJavaBaseTypes(graphSONTypeIdResolver); + } // Registers the GraphSON Module's types graphSONModule.getTypeDefinitions().forEach( diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java index ce87c79bf4..c6d29ad432 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java @@ -201,6 +201,7 @@ abstract class GraphSONModule extends TinkerPopJacksonModule { // need to explicitly add serializers for these types because Jackson doesn't do it at all. addSerializer(Integer.class, new GraphSONSerializersV4.IntegerGraphSONSerializer()); addSerializer(Double.class, new GraphSONSerializersV4.DoubleGraphSONSerializer()); + addSerializer(Float.class, new GraphSONSerializersV4.FloatGraphSONSerializer()); // traversal addSerializer(BulkSet.class, new TraversalSerializersV4.BulkSetJacksonSerializer()); @@ -229,10 +230,12 @@ abstract class GraphSONModule extends TinkerPopJacksonModule { // numbers addDeserializer(Integer.class, new GraphSONSerializersV4.IntegerJackonsDeserializer()); addDeserializer(Double.class, new GraphSONSerializersV4.DoubleJacksonDeserializer()); + addDeserializer(Float.class, new GraphSONSerializersV4.FloatJacksonDeserializer()); // traversal Stream.of( Direction.values(), + Merge.values(), T.values()).flatMap(Stream::of).forEach(e -> addDeserializer(e.getClass(), new TraversalSerializersV4.EnumJacksonDeserializer(e.getDeclaringClass()))); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java index 92817132cb..66361a0f05 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONSerializersV4.java @@ -341,6 +341,18 @@ class GraphSONSerializersV4 { } } + final static class FloatGraphSONSerializer extends StdScalarSerializer<Float> { + public FloatGraphSONSerializer() { + super(Float.class); + } + + @Override + public void serialize(final Float floatValue, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeNumber(floatValue); + } + } + /** * Maps in the JVM can have {@link Object} as a key, but in JSON they must be a {@link String}. */ @@ -627,6 +639,35 @@ class GraphSONSerializersV4 { } } + static class FloatJacksonDeserializer extends StdDeserializer<Float> { + + protected FloatJacksonDeserializer() { + super(Float.class); + } + + @Override + public Float deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + if (jsonParser.getCurrentToken().isNumeric()) + return jsonParser.getFloatValue(); + else { + final String numberText = jsonParser.getValueAsString(); + if ("NaN".equalsIgnoreCase(numberText)) + return Float.NaN; + else if ("-Infinity".equals(numberText) || "-INF".equalsIgnoreCase(numberText)) + return Float.NEGATIVE_INFINITY; + else if ("Infinity".equals(numberText) || "INF".equals(numberText)) + return Float.POSITIVE_INFINITY; + else + throw new IllegalStateException("Float value unexpected: " + numberText); + } + } + + @Override + public boolean isCachable() { + return true; + } + } + /** * When doing untyped serialization graph objects get a special "type" field appended. */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeSerializerV4.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeSerializerV4.java index 8f9029c1f3..d581dfe732 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeSerializerV4.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeSerializerV4.java @@ -18,33 +18,21 @@ */ package org.apache.tinkerpop.gremlin.structure.io.graphson; -import org.apache.tinkerpop.gremlin.process.traversal.Operator; -import org.apache.tinkerpop.gremlin.process.traversal.Order; import org.apache.tinkerpop.gremlin.process.traversal.Path; -import org.apache.tinkerpop.gremlin.process.traversal.Pick; -import org.apache.tinkerpop.gremlin.process.traversal.Pop; -import org.apache.tinkerpop.gremlin.process.traversal.SackFunctions; -import org.apache.tinkerpop.gremlin.process.traversal.Scope; -import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.step.util.BulkSet; import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree; -import org.apache.tinkerpop.gremlin.process.traversal.util.Metrics; -import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalMetrics; -import org.apache.tinkerpop.gremlin.structure.Column; import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Property; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.VertexProperty; -import org.apache.tinkerpop.gremlin.util.function.Lambda; import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator; import org.apache.tinkerpop.shaded.jackson.core.JsonToken; import org.apache.tinkerpop.shaded.jackson.core.type.WritableTypeId; import org.apache.tinkerpop.shaded.jackson.databind.jsontype.TypeIdResolver; import java.io.IOException; -import java.net.InetAddress; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; @@ -137,36 +125,12 @@ public class GraphSONTypeSerializerV4 extends AbstractGraphSONTypeSerializer { mapped = Path.class; else if (VertexProperty.class.isAssignableFrom(c)) mapped = VertexProperty.class; - else if (Metrics.class.isAssignableFrom(c)) - mapped = Metrics.class; - else if (TraversalMetrics.class.isAssignableFrom(c)) - mapped = TraversalMetrics.class; else if (Property.class.isAssignableFrom(c)) mapped = Property.class; else if (ByteBuffer.class.isAssignableFrom(c)) mapped = ByteBuffer.class; - else if (InetAddress.class.isAssignableFrom(c)) - mapped = InetAddress.class; - else if (Lambda.class.isAssignableFrom(c)) - mapped = Lambda.class; - else if (VertexProperty.Cardinality.class.isAssignableFrom(c)) - mapped = VertexProperty.Cardinality.class; - else if (Column.class.isAssignableFrom(c)) - mapped = Column.class; else if (Direction.class.isAssignableFrom(c)) mapped = Direction.class; - else if (Operator.class.isAssignableFrom(c)) - mapped = Operator.class; - else if (Order.class.isAssignableFrom(c)) - mapped = Order.class; - else if (Pop.class.isAssignableFrom(c)) - mapped = Pop.class; - else if (SackFunctions.Barrier.class.isAssignableFrom(c)) - mapped = SackFunctions.Barrier.class; - else if (Pick.class.isAssignableFrom(c)) - mapped = Pick.class; - else if (Scope.class.isAssignableFrom(c)) - mapped = Scope.class; else if (T.class.isAssignableFrom(c)) mapped = T.class; else diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapperEmbeddedTypeTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapperEmbeddedTypeTest.java index b37a70a39f..518e3ae557 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapperEmbeddedTypeTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapperEmbeddedTypeTest.java @@ -149,6 +149,20 @@ public class GraphSONMapperEmbeddedTypeTest extends AbstractGraphSONTest { assertEquals(o, serializeDeserialize(mapper, o, List.class)); } + @Test + public void shouldHandleFloatNumberConstants() throws Exception { + // Float serializer/deserializer only exists in V4 + assumeThat(version, startsWith("v4")); + + final List<Object> o = new ArrayList<>(); + o.add(123.321f); + o.add(Float.NaN); + o.add(Float.NEGATIVE_INFINITY); + o.add(Float.POSITIVE_INFINITY); + + assertEquals(o, serializeDeserialize(mapper, o, List.class)); + } + @Test public void shouldHandleMap() throws Exception { assumeThat(version, either(startsWith("v3")).or(startsWith("v4")));
