This is an automated email from the ASF dual-hosted git repository. Cole-Greer pushed a commit to branch simplePDT in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 38d3850ff755bfacc709beee1c90af255361ddde Author: Cole Greer <[email protected]> AuthorDate: Wed Jun 24 13:35:07 2026 -0700 Add GraphSON V4 serialization for PrimitivePDT (g:PrimitivePdt) Adds GraphSON V4 support for PrimitiveProviderDefinedType under the g:PrimitivePdt type tag. - PrimitiveProviderDefinedTypeJacksonSerializer emits {"type": <name>, "value": <value>} with value as an untyped JSON string (per the GraphSON spec; the value is the opaque stringified primitive). - PrimitiveProviderDefinedTypeJacksonDeserializer parses type/value and hydrates via the ProviderDefinedTypeRegistry when set. - GraphSONModule (V4) maps PrimitiveProviderDefinedType -> "PrimitivePdt", registers the ser/deser, and threads the registry to the primitive deserializer via setPdtRegistry. - Write-side adapter fallback (PdtGraphSONSerializerProviderV4 / GraphSONTypeIdResolver) extended so a raw object with a registered PrimitivePDTAdapter serializes as g:PrimitivePdt. Response-only in T4; both directions implemented for round-trip tests. Tests: PdtGraphSONSerializersV4Test extended with g:PrimitivePdt serialize/deserialize, registry hydration, and primitive-nested-in-composite. tinkerpop-2gy.4 Assisted-by: Kiro:claude-opus-4.8 --- .../structure/io/graphson/GraphSONModule.java | 7 ++ .../io/graphson/GraphSONTypeIdResolver.java | 3 + .../graphson/PdtGraphSONSerializerProviderV4.java | 11 ++- .../io/graphson/PdtGraphSONSerializersV4.java | 106 +++++++++++++++++++++ .../io/graphson/PdtGraphSONSerializersV4Test.java | 101 ++++++++++++++++++++ 5 files changed, 226 insertions(+), 2 deletions(-) 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 5cdb5cb1c3..40ca8ca59f 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 @@ -82,6 +82,7 @@ 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.structure.io.pdt.ProviderDefinedType; +import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry; import org.apache.tinkerpop.gremlin.structure.util.star.DirectionalStarGraph; import org.apache.tinkerpop.gremlin.structure.util.star.StarGraphGraphSONSerializerV1; @@ -159,6 +160,7 @@ abstract class GraphSONModule extends TinkerPopJacksonModule { put(VertexProperty.class, "VertexProperty"); put(Tree.class, "Tree"); put(ProviderDefinedType.class, "CompositePdt"); + put(PrimitiveProviderDefinedType.class, "PrimitivePdt"); Stream.of( Direction.class, Merge.class, @@ -166,6 +168,7 @@ abstract class GraphSONModule extends TinkerPopJacksonModule { }}); private final PdtGraphSONSerializersV4.ProviderDefinedTypeJacksonDeserializer pdtDeserializer; + private final PdtGraphSONSerializersV4.PrimitiveProviderDefinedTypeJacksonDeserializer primitivePdtDeserializer; /** * Constructs a new object. @@ -184,6 +187,7 @@ abstract class GraphSONModule extends TinkerPopJacksonModule { addSerializer(DirectionalStarGraph.class, new StarGraphGraphSONSerializerV4(normalize)); addSerializer(Tree.class, new GraphSONSerializersV4.TreeJacksonSerializer()); addSerializer(ProviderDefinedType.class, new PdtGraphSONSerializersV4.ProviderDefinedTypeJacksonSerializer()); + addSerializer(PrimitiveProviderDefinedType.class, new PdtGraphSONSerializersV4.PrimitiveProviderDefinedTypeJacksonSerializer()); // java.util - use the standard jackson serializers for collections when types aren't embedded if (typeInfo != TypeInfo.NO_TYPES) { @@ -216,6 +220,8 @@ abstract class GraphSONModule extends TinkerPopJacksonModule { addDeserializer(Tree.class, new GraphSONSerializersV4.TreeJacksonDeserializer()); pdtDeserializer = new PdtGraphSONSerializersV4.ProviderDefinedTypeJacksonDeserializer(); addDeserializer(ProviderDefinedType.class, pdtDeserializer); + primitivePdtDeserializer = new PdtGraphSONSerializersV4.PrimitiveProviderDefinedTypeJacksonDeserializer(); + addDeserializer(PrimitiveProviderDefinedType.class, primitivePdtDeserializer); // java.util - use the standard jackson serializers for collections when types aren't embedded if (typeInfo != TypeInfo.NO_TYPES) { @@ -242,6 +248,7 @@ abstract class GraphSONModule extends TinkerPopJacksonModule { void setPdtRegistry(final ProviderDefinedTypeRegistry registry) { pdtDeserializer.setRegistry(registry); + primitivePdtDeserializer.setRegistry(registry); } @Override diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java index 0b535f8141..cec70fb058 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java @@ -88,6 +88,9 @@ public class GraphSONTypeIdResolver implements TypeIdResolver { if (pdtRegistry != null && pdtRegistry.getAdapterByClass(aClass).isPresent()) { return typeToId.get(org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType.class); } + if (pdtRegistry != null && pdtRegistry.getPrimitiveAdapterByClass(aClass).isPresent()) { + return typeToId.get(org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType.class); + } // If one wants to serialize an object with a type, but hasn't registered // a typeID for that class, fail. throw new IllegalArgumentException(String.format("Could not find a type identifier for the class : %s. " + diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java index fb29453da5..008be0ec3b 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java @@ -33,20 +33,24 @@ final class PdtGraphSONSerializerProviderV4 extends DefaultSerializerProvider { private static final long serialVersionUID = 1L; private final ProviderDefinedTypeRegistry pdtRegistry; private final JsonSerializer<Object> pdtAdapterSerializer; + private final JsonSerializer<Object> primitivePdtAdapterSerializer; PdtGraphSONSerializerProviderV4(final ProviderDefinedTypeRegistry pdtRegistry) { super(); this.pdtRegistry = pdtRegistry; this.pdtAdapterSerializer = new PdtGraphSONSerializersV4.PdtAdapterJacksonSerializer(pdtRegistry); + this.primitivePdtAdapterSerializer = new PdtGraphSONSerializersV4.PrimitivePdtAdapterJacksonSerializer(pdtRegistry); } private PdtGraphSONSerializerProviderV4(final SerializerProvider src, final SerializationConfig config, final SerializerFactory f, final ProviderDefinedTypeRegistry pdtRegistry, - final JsonSerializer<Object> pdtAdapterSerializer) { + final JsonSerializer<Object> pdtAdapterSerializer, + final JsonSerializer<Object> primitivePdtAdapterSerializer) { super(src, config, f); this.pdtRegistry = pdtRegistry; this.pdtAdapterSerializer = pdtAdapterSerializer; + this.primitivePdtAdapterSerializer = primitivePdtAdapterSerializer; } @Override @@ -54,12 +58,15 @@ final class PdtGraphSONSerializerProviderV4 extends DefaultSerializerProvider { if (pdtRegistry != null && pdtRegistry.getAdapterByClass(aClass).isPresent()) { return pdtAdapterSerializer; } + if (pdtRegistry != null && pdtRegistry.getPrimitiveAdapterByClass(aClass).isPresent()) { + return primitivePdtAdapterSerializer; + } return super.getUnknownTypeSerializer(aClass); } @Override public PdtGraphSONSerializerProviderV4 createInstance(final SerializationConfig config, final SerializerFactory jsf) { - return new PdtGraphSONSerializerProviderV4(this, config, jsf, pdtRegistry, pdtAdapterSerializer); + return new PdtGraphSONSerializerProviderV4(this, config, jsf, pdtRegistry, pdtAdapterSerializer, primitivePdtAdapterSerializer); } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4.java index 5c0c72e1bf..193392f8b1 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4.java @@ -19,6 +19,8 @@ package org.apache.tinkerpop.gremlin.structure.io.graphson; import org.apache.tinkerpop.gremlin.structure.io.pdt.CompositePDTAdapter; +import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitivePDTAdapter; +import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry; @@ -118,6 +120,66 @@ final class PdtGraphSONSerializersV4 { } } + final static class PrimitiveProviderDefinedTypeJacksonSerializer extends StdScalarSerializer<PrimitiveProviderDefinedType> { + + public PrimitiveProviderDefinedTypeJacksonSerializer() { + super(PrimitiveProviderDefinedType.class); + } + + @Override + public void serialize(final PrimitiveProviderDefinedType pdt, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", pdt.getName()); + jsonGenerator.writeStringField("value", pdt.getValue()); + jsonGenerator.writeEndObject(); + } + } + + static class PrimitiveProviderDefinedTypeJacksonDeserializer extends StdDeserializer<PrimitiveProviderDefinedType> { + + private ProviderDefinedTypeRegistry registry; + + public PrimitiveProviderDefinedTypeJacksonDeserializer() { + super(PrimitiveProviderDefinedType.class); + } + + void setRegistry(final ProviderDefinedTypeRegistry registry) { + this.registry = registry; + } + + @Override + public PrimitiveProviderDefinedType deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) + throws IOException { + String typeName = null; + String value = null; + + while (jsonParser.nextToken() != JsonToken.END_OBJECT) { + final String fieldName = jsonParser.getCurrentName(); + jsonParser.nextToken(); + if ("type".equals(fieldName)) { + typeName = jsonParser.getText(); + } else if ("value".equals(fieldName)) { + value = jsonParser.getText(); + } + } + + final PrimitiveProviderDefinedType pdt = new PrimitiveProviderDefinedType(typeName, value); + if (registry != null) { + final Object hydrated = registry.hydratePrimitive(pdt); + if (hydrated instanceof PrimitiveProviderDefinedType) + return (PrimitiveProviderDefinedType) hydrated; + return pdt.withHydrated(hydrated); + } + return pdt; + } + + @Override + public boolean isCachable() { + return true; + } + } + /** * A serializer that converts raw objects to {@link ProviderDefinedType} using a registered adapter, * then serializes the resulting PDT in the standard CompositePdt format. @@ -168,4 +230,48 @@ final class PdtGraphSONSerializersV4 { return new ProviderDefinedType(adapter.typeName(), fields); } } + + /** + * A serializer that converts raw objects to {@link PrimitiveProviderDefinedType} using a registered + * {@link PrimitivePDTAdapter}, then serializes the result in the standard PrimitivePdt format. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + static class PrimitivePdtAdapterJacksonSerializer extends StdSerializer<Object> { + + private final ProviderDefinedTypeRegistry registry; + + PrimitivePdtAdapterJacksonSerializer(final ProviderDefinedTypeRegistry registry) { + super(Object.class); + this.registry = registry; + } + + @Override + public void serialize(final Object value, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) throws IOException { + final PrimitiveProviderDefinedType pdt = toPrimitivePdt(value); + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", pdt.getName()); + jsonGenerator.writeStringField("value", pdt.getValue()); + jsonGenerator.writeEndObject(); + } + + @Override + public void serializeWithType(final Object value, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider, + final org.apache.tinkerpop.shaded.jackson.databind.jsontype.TypeSerializer typeSerializer) throws IOException { + final PrimitiveProviderDefinedType pdt = toPrimitivePdt(value); + serializerProvider.findTypedValueSerializer(PrimitiveProviderDefinedType.class, true, null) + .serialize(pdt, jsonGenerator, serializerProvider); + } + + private PrimitiveProviderDefinedType toPrimitivePdt(final Object value) throws IOException { + final Optional<PrimitivePDTAdapter<?>> opt = registry.getPrimitiveAdapterByClass(value.getClass()); + if (!opt.isPresent()) { + throw new IOException("No primitive adapter found for " + value.getClass().getName()); + } + final PrimitivePDTAdapter adapter = opt.get(); + final String strValue = adapter.toValue(value); + return new PrimitiveProviderDefinedType(adapter.typeName(), strValue); + } + } } diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4Test.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4Test.java index 913d06bc51..f0fc71aff5 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4Test.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4Test.java @@ -19,6 +19,8 @@ package org.apache.tinkerpop.gremlin.structure.io.graphson; import org.apache.tinkerpop.gremlin.structure.io.pdt.CompositePDTAdapter; +import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitivePDTAdapter; +import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry; import org.apache.tinkerpop.shaded.jackson.databind.JsonNode; @@ -248,4 +250,103 @@ public class PdtGraphSONSerializersV4Test extends AbstractGraphSONTest { assertEquals("Unknown", result.getName()); assertEquals(1, result.getFields().get("a")); } + + // --- PrimitivePDT tests --- + + @Test + public void shouldSerializePrimitivePdt() throws Exception { + final PrimitiveProviderDefinedType pdt = new PrimitiveProviderDefinedType("Duration", "PT5M"); + + final String json = mapper.writeValueAsString(pdt); + final JsonNode node = plainMapper.readTree(json); + + assertEquals("g:PrimitivePdt", node.get("@type").asText()); + final JsonNode value = node.get("@value"); + assertEquals("Duration", value.get("type").asText()); + // value must be an untyped JSON string (no @type/@value wrapping) + assertTrue(value.get("value").isTextual()); + assertEquals("PT5M", value.get("value").asText()); + } + + @Test + public void shouldDeserializePrimitivePdt() throws Exception { + final String json = "{\"@type\":\"g:PrimitivePdt\",\"@value\":{\"type\":\"Duration\",\"value\":\"PT5M\"}}"; + final PrimitiveProviderDefinedType pdt = mapper.readValue(json, PrimitiveProviderDefinedType.class); + + assertEquals("Duration", pdt.getName()); + assertEquals("PT5M", pdt.getValue()); + } + + @Test + public void shouldRoundTripPrimitivePdt() throws Exception { + final PrimitiveProviderDefinedType original = new PrimitiveProviderDefinedType("Duration", "PT5M"); + final PrimitiveProviderDefinedType result = serializeDeserialize(mapper, original, PrimitiveProviderDefinedType.class); + + assertEquals(original.getName(), result.getName()); + assertEquals(original.getValue(), result.getValue()); + } + + @Test + public void shouldHydratePrimitivePdtWhenRegistryConfigured() throws Exception { + final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty(); + registry.register(new DurationAdapter()); + + final ObjectMapper hydratingMapper = GraphSONMapper.build() + .version(GraphSONVersion.V4_0) + .addCustomModule(GraphSONXModuleV4.build()) + .typeInfo(TypeInfo.PARTIAL_TYPES) + .pdtRegistry(registry) + .create().createMapper(); + + final PrimitiveProviderDefinedType pdt = new PrimitiveProviderDefinedType("Duration", "PT10S"); + final PrimitiveProviderDefinedType result = serializeDeserialize(hydratingMapper, pdt, PrimitiveProviderDefinedType.class); + + assertNotNull(result.getHydrated()); + assertTrue(result.getHydrated() instanceof MyDuration); + assertEquals(10, ((MyDuration) result.getHydrated()).seconds); + } + + @Test + public void shouldNestPrimitivePdtInsideCompositePdt() throws Exception { + final PrimitiveProviderDefinedType inner = new PrimitiveProviderDefinedType("Duration", "PT1H"); + final Map<String, Object> outerFields = new LinkedHashMap<>(); + outerFields.put("name", "timeout"); + outerFields.put("dur", inner); + final ProviderDefinedType outer = new ProviderDefinedType("Config", outerFields); + + final String json = mapper.writeValueAsString(outer); + final JsonNode node = plainMapper.readTree(json); + + assertEquals("g:CompositePdt", node.get("@type").asText()); + final JsonNode durNode = node.get("@value").get("fields").get("dur"); + assertEquals("g:PrimitivePdt", durNode.get("@type").asText()); + assertEquals("Duration", durNode.get("@value").get("type").asText()); + assertEquals("PT1H", durNode.get("@value").get("value").asText()); + + // round-trip + final ProviderDefinedType result = serializeDeserialize(mapper, outer, ProviderDefinedType.class); + assertEquals("Config", result.getName()); + assertTrue(result.getFields().get("dur") instanceof PrimitiveProviderDefinedType); + final PrimitiveProviderDefinedType nestedResult = (PrimitiveProviderDefinedType) result.getFields().get("dur"); + assertEquals("Duration", nestedResult.getName()); + assertEquals("PT1H", nestedResult.getValue()); + } + + // helper types for primitive PDT hydration tests + + static class MyDuration { + final int seconds; + MyDuration(int seconds) { this.seconds = seconds; } + } + + static class DurationAdapter implements PrimitivePDTAdapter<MyDuration> { + @Override public String typeName() { return "Duration"; } + @Override public Class<MyDuration> targetClass() { return MyDuration.class; } + @Override public String toValue(MyDuration obj) { return "PT" + obj.seconds + "S"; } + @Override public MyDuration fromValue(String value) { + // parse PTnS + final String stripped = value.replaceAll("[PTS]", "").replace("H", "").replace("M", ""); + return new MyDuration(Integer.parseInt(stripped)); + } + } }
