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

colegreer pushed a commit to branch 3.8-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/3.8-dev by this push:
     new 4ce4717925 Add custom type serializer API for gremlin-go (#3335)
4ce4717925 is described below

commit 4ce47179257d84740f8cb9d1bd5a23d03658061d
Author: DR1N0 <[email protected]>
AuthorDate: Thu Mar 26 08:49:04 2026 +0800

    Add custom type serializer API for gremlin-go (#3335)
    
    Adds support for serializing custom types in gremlin-go, bringing it to 
feature parity with the Java driver and completing the custom type support that 
currently only includes deserialization. This implements the official 
GraphBinary specification for custom types, enabling full round-trip support 
for custom graph database types.
---
 CHANGELOG.asciidoc                   |  1 +
 gremlin-go/driver/graphBinary.go     | 57 ++++++++++++++++++++++
 gremlin-go/driver/serializer.go      | 37 ++++++++++++++
 gremlin-go/driver/serializer_test.go | 95 ++++++++++++++++++++++++++++++++++++
 4 files changed, 190 insertions(+)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 60364cd4b7..2fdab0f0e8 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -36,6 +36,7 @@ This release also includes changes from <<release-3-7-6, 
3.7.6>>.
 * Expose serialization functions for alternative transport protocols in 
gremlin-go
 * Improved Gremlint formatting to keep the first argument for a step on the 
same line if line breaks were required to meet max line length.
 * Improved Gremlint formatting to do greedy argument packing when possible so 
that more arguments can appear on a single line.
+* Add custom type writer and serializer to gremlin-go
 
 [[release-3-8-0]]
 === TinkerPop 3.8.0 (Release Date: November 12, 2025)
diff --git a/gremlin-go/driver/graphBinary.go b/gremlin-go/driver/graphBinary.go
index 8f98ebdcca..6c6a888fb0 100644
--- a/gremlin-go/driver/graphBinary.go
+++ b/gremlin-go/driver/graphBinary.go
@@ -681,7 +681,57 @@ func bindingWriter(value interface{}, buffer 
*bytes.Buffer, typeSerializer *grap
        return buffer.Bytes(), nil
 }
 
+// customTypeWriter handles serialization of custom types registered via 
RegisterCustomTypeWriter.
+// Format: {type_code}{type_name}{custom_payload}
+// where:
+//   - type_code = 0x00 (customType) - already written in write() before 
invoking this function
+//   - type_name = string with length prefix - written by this function
+//   - custom_payload = everything else - written by the user's 
CustomTypeWriter function
+//
+// The custom_payload typically includes {value_flag}{value}, but custom types 
may include
+// additional metadata (e.g., JanusGraph's customTypeInfo) before the 
value_flag.
+func customTypeWriter(value interface{}, buffer *bytes.Buffer, typeSerializer 
*graphBinaryTypeSerializer) ([]byte, error) {
+       // Look up the custom type info
+       valType := reflect.TypeOf(value)
+       customTypeWriterLock.RLock()
+       typeInfo, exists := customSerializers[valType]
+       customTypeWriterLock.RUnlock()
+
+       if !exists || customSerializers == nil {
+               return nil, 
newError(err0407GetSerializerToWriteUnknownTypeError, valType.Name())
+       }
+
+       // Write the custom type name as a String (length prefix + UTF-8 bytes)
+       typeName := typeInfo.TypeName
+       typeNameBytes := []byte(typeName)
+       if err := binary.Write(buffer, binary.BigEndian, 
int32(len(typeNameBytes))); err != nil {
+               return nil, err
+       }
+       if _, err := buffer.Write(typeNameBytes); err != nil {
+               return nil, err
+       }
+
+       // Call the custom writer to serialize the value
+       if err := typeInfo.Writer(value, buffer, typeSerializer); err != nil {
+               return nil, err
+       }
+
+       return buffer.Bytes(), nil
+}
+
 func (serializer *graphBinaryTypeSerializer) getType(val interface{}) 
(dataType, error) {
+       // Check if this is a registered custom type
+       valType := reflect.TypeOf(val)
+       customTypeWriterLock.RLock()
+       var isCustomType bool
+       if customSerializers != nil {
+               _, isCustomType = customSerializers[valType]
+       }
+       customTypeWriterLock.RUnlock()
+       if isCustomType {
+               return customType, nil
+       }
+
        switch val.(type) {
        case *Bytecode, Bytecode, *GraphTraversal:
                return bytecodeType, nil
@@ -816,6 +866,13 @@ func (serializer *graphBinaryTypeSerializer) 
write(valueObject interface{}, buff
                return nil, err
        }
        buffer.Write(dataType.getCodeBytes())
+       if dataType == customType {
+               // Custom type format typically: 
{type_code=0x00}{type_name}{custom_writer_output}
+               // The type_name immediately follows type_code with NO 
value_flag in between.
+               // writeType would insert an extra value_flag byte that shifts 
the type_name
+               // string, causing the server to compute the wrong string 
length → PROCESSING_ERROR.
+               return writer(valueObject, buffer, serializer)
+       }
        return serializer.writeType(valueObject, buffer, writer)
 }
 
diff --git a/gremlin-go/driver/serializer.go b/gremlin-go/driver/serializer.go
index cdcf1c7dd9..e3549d2404 100644
--- a/gremlin-go/driver/serializer.go
+++ b/gremlin-go/driver/serializer.go
@@ -46,6 +46,15 @@ type GraphBinarySerializer struct {
 // CustomTypeReader user provided function to deserialize custom types
 type CustomTypeReader func(data *[]byte, i *int) (interface{}, error)
 
+// CustomTypeWriter user provided function to serialize custom types
+type CustomTypeWriter func(value interface{}, buffer *bytes.Buffer, serializer 
*graphBinaryTypeSerializer) error
+
+// CustomTypeInfo holds metadata for a registered custom type
+type CustomTypeInfo struct {
+       TypeName string
+       Writer   CustomTypeWriter
+}
+
 type writer func(interface{}, *bytes.Buffer, *graphBinaryTypeSerializer) 
([]byte, error)
 type reader func(data *[]byte, i *int) (interface{}, error)
 
@@ -56,6 +65,10 @@ var serializers map[dataType]writer
 var customTypeReaderLock = sync.RWMutex{}
 var customDeserializers map[string]CustomTypeReader
 
+// customTypeWriterLock used to synchronize access to the customSerializers map
+var customTypeWriterLock = sync.RWMutex{}
+var customSerializers map[reflect.Type]CustomTypeInfo
+
 func init() {
        initSerializers()
        initDeserializers()
@@ -266,6 +279,7 @@ func (gs GraphBinarySerializer) DeserializeMessage(message 
[]byte) (Response, er
 
 func initSerializers() {
        serializers = map[dataType]writer{
+               customType:     customTypeWriter,
                bytecodeType:   bytecodeWriter,
                stringType:     stringWriter,
                bigDecimalType: bigDecimalWriter,
@@ -392,3 +406,26 @@ func UnregisterCustomTypeReader(customTypeName string) {
        defer customTypeReaderLock.Unlock()
        delete(customDeserializers, customTypeName)
 }
+
+// RegisterCustomTypeWriter registers a writer (serializer) for a custom type.
+// The valueType should be the reflect.Type of the custom type (e.g., 
reflect.TypeOf((*MyType)(nil)))
+// The typeName is the GraphBinary custom type name (e.g., 
"janusgraph.RelationIdentifier")
+// The writer function should serialize the value into the buffer in 
GraphBinary custom type format.
+func RegisterCustomTypeWriter(valueType reflect.Type, typeName string, writer 
CustomTypeWriter) {
+       customTypeWriterLock.Lock()
+       defer customTypeWriterLock.Unlock()
+       if customSerializers == nil {
+               customSerializers = make(map[reflect.Type]CustomTypeInfo)
+       }
+       customSerializers[valueType] = CustomTypeInfo{
+               TypeName: typeName,
+               Writer:   writer,
+       }
+}
+
+// UnregisterCustomTypeWriter unregisters a writer (serializer) for a custom 
type
+func UnregisterCustomTypeWriter(valueType reflect.Type) {
+       customTypeWriterLock.Lock()
+       defer customTypeWriterLock.Unlock()
+       delete(customSerializers, valueType)
+}
diff --git a/gremlin-go/driver/serializer_test.go 
b/gremlin-go/driver/serializer_test.go
index b2f3a03bbc..945f826439 100644
--- a/gremlin-go/driver/serializer_test.go
+++ b/gremlin-go/driver/serializer_test.go
@@ -20,8 +20,11 @@ under the License.
 package gremlingo
 
 import (
+       "bytes"
+       "encoding/binary"
        "errors"
        "fmt"
+       "reflect"
        "testing"
 
        "github.com/google/uuid"
@@ -78,6 +81,45 @@ func TestSerializer(t *testing.T) {
                assert.Equal(t, map[string]interface{}{}, 
response.ResponseResult.Meta)
                assert.NotNil(t, response.ResponseResult.Data)
        })
+
+       t.Run("test serialized request message w/ custom type", func(t 
*testing.T) {
+               customType := reflect.TypeOf((*TestCustomType)(nil))
+               typeName := "test.CustomType"
+
+               // Register the custom type writer
+               RegisterCustomTypeWriter(customType, typeName, 
testCustomTypeWriter)
+               defer UnregisterCustomTypeWriter(customType)
+
+               testValue := &TestCustomType{
+                       ID:    12345,
+                       Value: "test value",
+               }
+
+               var u, _ = uuid.Parse("41d2e28a-20a4-4ab0-b379-d810dede3786")
+               testRequest := request{
+                       requestID: u,
+                       op:        "eval",
+                       processor: "",
+                       args:      map[string]interface{}{"gremlin": 
"g.V().count()", "customArg": testValue},
+               }
+
+               serializer := 
newGraphBinarySerializer(newLogHandler(&defaultLogger{}, Error, 
language.English))
+               serialized, err := serializer.SerializeMessage(&testRequest)
+
+               assert.Nil(t, err)
+               assert.NotNil(t, serialized)
+
+               // Verify the serialized data contains the custom type name 
bytes
+               typeNameBytes := []byte(typeName)
+               found := false
+               for i := 0; i <= len(serialized)-len(typeNameBytes); i++ {
+                       if bytes.Equal(serialized[i:i+len(typeNameBytes)], 
typeNameBytes) {
+                               found = true
+                               break
+                       }
+               }
+               assert.True(t, found, "Expected serialized data to contain 
custom type name")
+       })
 }
 
 func TestSerializerFailures(t *testing.T) {
@@ -106,6 +148,59 @@ func TestSerializerFailures(t *testing.T) {
                assert.NotNil(t, err)
                assert.True(t, 
isSameErrorCode(newError(err0409GetSerializerToReadUnknownCustomTypeError), 
err))
        })
+
+       t.Run("test unregistered custom type writer failure", func(t 
*testing.T) {
+               type UnregisteredType struct {
+                       Value string
+               }
+
+               testValue := &UnregisteredType{Value: "test"}
+
+               var u, _ = uuid.Parse("41d2e28a-20a4-4ab0-b379-d810dede3786")
+               testRequest := request{
+                       requestID: u,
+                       op:        "eval",
+                       processor: "",
+                       args:      map[string]interface{}{"gremlin": 
"g.V().count()", "unregistered": testValue},
+               }
+
+               serializer := 
newGraphBinarySerializer(newLogHandler(&defaultLogger{}, Error, 
language.English))
+               serialized, err := serializer.SerializeMessage(&testRequest)
+
+               assert.Nil(t, serialized)
+               assert.NotNil(t, err)
+               assert.True(t, 
isSameErrorCode(newError(err0407GetSerializerToWriteUnknownTypeError), err))
+       })
+}
+
+// TestCustomType is a test custom type for writer tests
+type TestCustomType struct {
+       ID    int64
+       Value string
+}
+
+// testCustomTypeWriter is a writer for the test custom type
+var testCustomTypeWriter = func(value interface{}, buffer *bytes.Buffer, _ 
*graphBinaryTypeSerializer) error {
+       customValue, ok := value.(*TestCustomType)
+       if !ok {
+               return errors.New("expected *TestCustomType")
+       }
+
+       // Write ID as int64
+       if err := binary.Write(buffer, binary.BigEndian, customValue.ID); err 
!= nil {
+               return err
+       }
+
+       // Write Value as string (length-prefixed)
+       valueBytes := []byte(customValue.Value)
+       if err := binary.Write(buffer, binary.BigEndian, 
int32(len(valueBytes))); err != nil {
+               return err
+       }
+       if _, err := buffer.Write(valueBytes); err != nil {
+               return err
+       }
+
+       return nil
 }
 
 // exampleJanusgraphRelationIdentifierReader this implementation is not 
complete and is used only for the purposes of testing custom readers

Reply via email to