diff --git a/gateway/schema/exports_test.go b/gateway/schema/exports_test.go index 40c2aab4..9c099ccd 100644 --- a/gateway/schema/exports_test.go +++ b/gateway/schema/exports_test.go @@ -3,6 +3,7 @@ package schema import "k8s.io/apimachinery/pkg/runtime/schema" var StringMapScalarForTest = stringMapScalar +var JSONStringScalarForTest = jsonStringScalar func GetGatewayForTest(typeNameRegistry map[string]string) *Gateway { return &Gateway{ diff --git a/gateway/schema/scalars.go b/gateway/schema/scalars.go index 97e02063..88c2a419 100644 --- a/gateway/schema/scalars.go +++ b/gateway/schema/scalars.go @@ -1,6 +1,8 @@ package schema import ( + "encoding/json" + "github.com/graphql-go/graphql" "github.com/graphql-go/graphql/language/ast" ) @@ -34,3 +36,39 @@ var stringMapScalar = graphql.NewScalar(graphql.ScalarConfig{ } }, }) + +var jsonStringScalar = graphql.NewScalar(graphql.ScalarConfig{ + Name: "JSONString", + Description: "A JSON-serialized string representation of any object.", + Serialize: func(value interface{}) interface{} { + // Convert the value to JSON string + jsonBytes, err := json.Marshal(value) + if err != nil { + // Fallback to empty JSON object if marshaling fails + return "{}" + } + return string(jsonBytes) + }, + ParseValue: func(value interface{}) interface{} { + if str, ok := value.(string); ok { + var result interface{} + err := json.Unmarshal([]byte(str), &result) + if err != nil { + return nil // Invalid JSON + } + return result + } + return nil + }, + ParseLiteral: func(valueAST ast.Value) interface{} { + if value, ok := valueAST.(*ast.StringValue); ok { + var result interface{} + err := json.Unmarshal([]byte(value.Value), &result) + if err != nil { + return nil // Invalid JSON + } + return result + } + return nil + }, +}) diff --git a/gateway/schema/scalars_test.go b/gateway/schema/scalars_test.go index 1ab9616c..8f20a5ca 100644 --- a/gateway/schema/scalars_test.go +++ b/gateway/schema/scalars_test.go @@ -1,6 +1,7 @@ package schema_test import ( + "encoding/json" "reflect" "testing" @@ -153,3 +154,51 @@ func TestGenerateTypeName(t *testing.T) { }) } } + +func TestJSONStringScalar_ProperSerialization(t *testing.T) { + testObject := map[string]interface{}{ + "name": "example-config", + "namespace": "default", + "labels": map[string]string{ + "hello": "world", + }, + "annotations": map[string]string{ + "kcp.io/cluster": "root", + }, + } + + // Test the JSONString scalar serialization + result := schema.JSONStringScalarForTest.Serialize(testObject) + + if result == nil { + t.Fatal("JSONStringScalar.Serialize returned nil") + } + + resultStr, ok := result.(string) + if !ok { + t.Fatalf("JSONStringScalar.Serialize returned %T, expected string", result) + } + + // Verify it's valid JSON + var parsed map[string]interface{} + err := json.Unmarshal([]byte(resultStr), &parsed) + if err != nil { + t.Fatalf("Result is not valid JSON: %s\nResult: %s", err, resultStr) + } + + // Verify the content is preserved + if parsed["name"] != "example-config" { + t.Errorf("Name not preserved: got %v, want %v", parsed["name"], "example-config") + } + + if parsed["namespace"] != "default" { + t.Errorf("Namespace not preserved: got %v, want %v", parsed["namespace"], "default") + } + + // Verify it's NOT Go map format + if len(resultStr) > 10 && resultStr[:4] == "map[" { + t.Errorf("Result is in Go map format, not JSON: %s", resultStr) + } + + t.Logf("Proper JSON output: %s", resultStr) +} diff --git a/gateway/schema/schema.go b/gateway/schema/schema.go index 96e18c3b..9085ec27 100644 --- a/gateway/schema/schema.go +++ b/gateway/schema/schema.go @@ -453,8 +453,8 @@ func (g *Gateway) handleObjectFieldSpecType(fieldSpec spec.Schema, typePrefix st } } - // It's an empty object - return graphql.String, graphql.String, nil + // It's an empty object, serialize as JSON string + return jsonStringScalar, jsonStringScalar, nil } func (g *Gateway) generateTypeName(typePrefix string, fieldPath []string) string {