Repository: goadesign/goa

Issue:
Support for Protobuf `Any` type in gRPC DSL
When defining a Goa service with gRPC, using the `Any` type in the payload results in an error, even though Protobuf provides native support for `Any` via the [Well Known Types library](https://protobuf.dev/reference/protobuf/google.protobuf/#any)


**Example Code:**

```go
package v2

import (
	. "goa.design/goa/v3/dsl"
)

var _ = Service("example", func() {
	Description("An example service")

	Method("hello", func() {
		Description("Says hello")

		Payload(func() {
			Field(1, "object", Any, "Object to greet")
		})

		HTTP(func() {
			GET("/hello")
		})

		GRPC(func() {
			Response(CodeOK)
		})

		Result(String)
	})
})
```

**Error:**

```
service "example" gRPC endpoint "hello": Attribute "object" is Any type which is not supported in gRPC
```

**Expected Behavior:**


Since Protobuf already supports the `Any` type, Goa should ideally allow the use of `Any` in gRPC payloads and map it to `google.protobuf.Any`.

**Question/Feature Request:**


Can support for Protobuf `Any` be added to Goa’s gRPC DSL so that attributes declared as `Any` map to `google.protobuf.Any` in the generated `.proto` files?

Discussion:
@raphael: Makes sense, I'd be happy to merge a PR that added this capability.

Solution patch:
diff --git a/dsl/types.go b/dsl/types.go
index bbbd7a75..63087167 100644
--- a/dsl/types.go
+++ b/dsl/types.go
@@ -37,6 +37,9 @@ const (
 	Bytes = expr.Bytes
 
 	// Any is the type for an arbitrary JSON value (any in Go).
+	// In gRPC, Any is mapped to google.protobuf.Any using JSON encoding/decoding.
+	// The value is JSON-marshaled when converting from Go to protobuf, and
+	// JSON-unmarshaled when converting from protobuf to Go.
 	Any = expr.Any
 )
 
diff --git a/expr/grpc_endpoint.go b/expr/grpc_endpoint.go
index 6c9b8d8b..c6ff3d32 100644
--- a/expr/grpc_endpoint.go
+++ b/expr/grpc_endpoint.go
@@ -152,14 +152,6 @@ func (e *GRPCEndpointExpr) Validate() error {
 		verr.Add(e, "Endpoint name cannot be empty")
 	}
 
-	// error if payload, result, and error type define attribute of Any type
-	// which is unsupported.
-	verr.Merge(e.hasAnyType(e.MethodExpr.Payload, "Payload"))
-	verr.Merge(e.hasAnyType(e.MethodExpr.Result, "Result"))
-	for _, er := range e.MethodExpr.Errors {
-		verr.Merge(e.hasAnyType(er.AttributeExpr, fmt.Sprintf("Error %q", er.Name)))
-	}
-
 	var hasMessage, hasMetadata bool
 	// Validate request
 	if e.Request.Type != Empty {
@@ -523,70 +515,4 @@ func getSecurityAttributes(m *MethodExpr) []string {
 	return secAttrs
 }
 
-// hasAnyType recurses through the given attribute and returns validation error
-// if any attribute is of Any type.
-func (e *GRPCEndpointExpr) hasAnyType(a *AttributeExpr, typ string, seen ...map[string]struct{}) *eval.ValidationErrors {
-	verr := new(eval.ValidationErrors)
-	if a.Type == Any {
-		verr.Add(e, "%s type is Any type which is not supported in gRPC", typ)
-	}
-	switch actual := a.Type.(type) {
-	case UserType:
-		var s map[string]struct{}
-		if len(seen) > 0 {
-			s = seen[0]
-		} else {
-			s = make(map[string]struct{})
-			seen = append(seen, s)
-		}
-		if _, ok := s[actual.ID()]; ok {
-			return verr
-		}
-		s[actual.ID()] = struct{}{}
-		verr.Merge(e.hasAnyType(actual.Attribute(), typ, seen...))
-	case *Array:
-		if IsPrimitive(actual.ElemType.Type) {
-			if actual.ElemType.Type == Any {
-				verr.Add(e, "Array element type is Any type which is not supported in gRPC")
-			}
-			return verr
-		}
-		verr.Merge(e.hasAnyType(actual.ElemType, typ, seen...))
-	case *Map:
-		if IsPrimitive(actual.KeyType.Type) {
-			if actual.KeyType.Type == Any {
-				verr.Add(e, "Map key type is Any type which is not supported in gRPC")
-			}
-		} else {
-			verr.Merge(e.hasAnyType(actual.KeyType, typ, seen...))
-		}
-		if IsPrimitive(actual.ElemType.Type) {
-			if actual.ElemType.Type == Any {
-				verr.Add(e, "Map element type is Any type which is not supported in gRPC")
-			}
-			return verr
-		}
-		verr.Merge(e.hasAnyType(actual.ElemType, typ, seen...))
-	case *Object:
-		for _, nat := range *actual {
-			if IsPrimitive(nat.Attribute.Type) {
-				if nat.Attribute.Type == Any {
-					verr.Add(e, "Attribute %q is Any type which is not supported in gRPC", nat.Name)
-				}
-				continue
-			}
-			verr.Merge(e.hasAnyType(nat.Attribute, typ, seen...))
-		}
-	case *Union:
-		for _, nat := range actual.Values {
-			if IsPrimitive(nat.Attribute.Type) {
-				if nat.Attribute.Type == Any {
-					verr.Add(e, "Union member %q is Any type which is not supported in gRPC", nat.Name)
-				}
-				continue
-			}
-			verr.Merge(e.hasAnyType(nat.Attribute, typ, seen...))
-		}
-	}
-	return verr
-}
+
diff --git a/grpc/codegen/client_cli.go b/grpc/codegen/client_cli.go
index 37c0970a..3665c207 100644
--- a/grpc/codegen/client_cli.go
+++ b/grpc/codegen/client_cli.go
@@ -61,6 +61,26 @@ func endpointParser(genpkg string, services *ServicesData, svr *expr.ServerExpr,
 		codegen.GoaNamedImport("grpc", "goagrpc"),
 		{Path: "google.golang.org/grpc", Name: "grpc"},
 	}
+	// Add anypb and structpb imports if Any type is used
+	needsAnyPb := false
+	for _, svc := range services.Root.API.GRPC.Services {
+		for _, e := range svc.GRPCEndpoints {
+			if hasAnyType(e.MethodExpr.Payload) || hasAnyType(e.MethodExpr.Result) {
+				needsAnyPb = true
+				break
+			}
+		}
+		if needsAnyPb {
+			break
+		}
+	}
+	if needsAnyPb {
+		specs = append(specs, 
+			&codegen.ImportSpec{Path: "encoding/json"},
+			&codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/anypb", Name: "anypb"},
+			&codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/structpb", Name: "structpb"},
+		)
+	}
 	for _, svc := range services.Root.API.GRPC.Services {
 		sd := services.Get(svc.Name())
 		if sd == nil {
@@ -117,6 +137,20 @@ func payloadBuilders(genpkg string, svc *expr.GRPCServiceExpr, data *cli.Command
 		{Path: path.Join(genpkg, svcName), Name: sd.Service.PkgName},
 		{Path: path.Join(genpkg, "grpc", svcName, pbPkgName), Name: sd.PkgName},
 	}
+	// Add anypb and structpb imports if Any type is used
+	needsAnyPb := false
+	for _, e := range svc.GRPCEndpoints {
+		if hasAnyType(e.MethodExpr.Payload) || hasAnyType(e.MethodExpr.Result) {
+			needsAnyPb = true
+			break
+		}
+	}
+	if needsAnyPb {
+		specs = append(specs, 
+			&codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/anypb", Name: "anypb"},
+			&codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/structpb", Name: "structpb"},
+		)
+	}
 	sections := []*codegen.SectionTemplate{
 		codegen.Header(title, "client", specs),
 	}
diff --git a/grpc/codegen/client_types.go b/grpc/codegen/client_types.go
index 004c1868..aaa3630f 100644
--- a/grpc/codegen/client_types.go
+++ b/grpc/codegen/client_types.go
@@ -75,6 +75,28 @@ func clientType(genpkg string, svc *expr.GRPCServiceExpr, services *ServicesData
 			{Path: path.Join(genpkg, svcName, "views"), Name: sd.Service.ViewsPkg},
 			{Path: path.Join(genpkg, "grpc", svcName, pbPkgName), Name: sd.PkgName},
 		}
+		// Add imports if Any type is used
+		needsAnyTypeImports := false
+		for _, e := range svc.GRPCEndpoints {
+			if hasAnyType(e.MethodExpr.Payload) || hasAnyType(e.MethodExpr.Result) {
+				needsAnyTypeImports = true
+				break
+			}
+			for _, er := range e.MethodExpr.Errors {
+				if hasAnyType(er.AttributeExpr) {
+					needsAnyTypeImports = true
+					break
+				}
+			}
+			if needsAnyTypeImports {
+				break
+			}
+		}
+		if needsAnyTypeImports {
+			imports = append(imports, &codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/anypb", Name: "anypb"})
+			imports = append(imports, &codegen.ImportSpec{Path: "encoding/json"})
+			imports = append(imports, &codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/structpb", Name: "structpb"})
+		}
 		imports = append(imports, sd.Service.ProtoImports...)
 		sections = []*codegen.SectionTemplate{codegen.Header(svc.Name()+" gRPC client types", "client", imports)}
 		for _, init := range initData {
diff --git a/grpc/codegen/protobuf.go b/grpc/codegen/protobuf.go
index 46182fc1..62b818b5 100644
--- a/grpc/codegen/protobuf.go
+++ b/grpc/codegen/protobuf.go
@@ -440,6 +440,8 @@ func protoNativeType(t expr.DataType) string {
 		return "string"
 	case expr.BytesKind:
 		return "bytes"
+	case expr.AnyKind:
+		return "google.protobuf.Any"
 	default:
 		panic(fmt.Sprintf("cannot compute native protocol buffer type for %T", t)) // bug
 	}
@@ -472,6 +474,8 @@ func protoBufNativeGoTypeName(t expr.DataType) string {
 		return "string"
 	case expr.BytesKind:
 		return "[]byte"
+	case expr.AnyKind:
+		return "*anypb.Any"
 	default:
 		panic(fmt.Sprintf("cannot compute native protocol buffer type for %T %v", t, t)) // bug
 	}
diff --git a/grpc/codegen/protobuf_transform.go b/grpc/codegen/protobuf_transform.go
index fe157e53..9c94b3e5 100644
--- a/grpc/codegen/protobuf_transform.go
+++ b/grpc/codegen/protobuf_transform.go
@@ -179,6 +179,14 @@ func transformAttribute(source, target *expr.AttributeExpr, sourceVar, targetVar
 			} else {
 				code, err = transformUnionFromProto(source, target, sourceVar, targetVar, ta)
 			}
+		case source.Type.Kind() == expr.AnyKind || target.Type.Kind() == expr.AnyKind:
+			// Special handling for Any type conversions
+			assign := "="
+			if newVar {
+				assign = ":="
+			}
+			srcField := convertType(source, target, false, false, sourceVar, ta)
+			code = fmt.Sprintf("%s %s %s\n", targetVar, assign, srcField)
 		default:
 			assign := "="
 			if newVar {
@@ -634,8 +642,8 @@ func convertType(src, tgt *expr.AttributeExpr, srcPtr bool, tgtPtr bool, srcVar
 
 	srcType, _ := codegen.GetMetaType(src)
 	tgtType, _ := codegen.GetMetaType(tgt)
-	if srcType == "" && tgtType == "" && (src.Type != expr.Int) && (src.Type != expr.UInt) {
-		// Nothing to do
+	if srcType == "" && tgtType == "" && (src.Type != expr.Int) && (src.Type != expr.UInt) && (src.Type != expr.Any) {
+		// Nothing to do, except for Any type which needs special conversion
 		return srcVar
 	}
 
@@ -645,11 +653,50 @@ func convertType(src, tgt *expr.AttributeExpr, srcPtr bool, tgtPtr bool, srcVar
 	return convertPrimitiveFromProto(src, tgt, srcPtr, tgtPtr, srcVar, ta)
 }
 
+const convertGoAnyToProtobufAnyFunc = `func() *anypb.Any {
+	if %s == nil {
+		return nil
+	}
+	// Convert Go any to protobuf Any using JSON marshaling
+	if jsonData, err := json.Marshal(%s); err == nil {
+		if data, err := anypb.New(&structpb.Value{
+			Kind: &structpb.Value_StringValue{StringValue: string(jsonData)},
+		}); err == nil {
+			return data
+		}
+	}
+	return nil
+}()`
+
+const convertProtobufAnyToGoAnyFunc = `func() any {
+	if %s != nil {
+		var value structpb.Value
+		if err := %s.UnmarshalTo(&value); err == nil {
+			if str := value.GetStringValue(); str != "" {
+				var result any
+				if err := json.Unmarshal([]byte(str), &result); err == nil {
+					return result
+				}
+			}
+		}
+	}
+	return nil
+}()`
+
 // convertPrimitive returns the code to convert a primitive type from one
 // representation to another.
 // NOTE: For Int and UInt kinds, protocol buffer Go compiler generates
 // int32 and uint32 respectively whereas Goa generates int and uint.
 func convertPrimitiveToProto(_, tgt *expr.AttributeExpr, srcPtr, _ bool, srcVar string, _ *transformAttrs) string {
+	// Special handling for Any type conversion to google.protobuf.Any
+	if tgt.Type.Kind() == expr.AnyKind {
+		if srcPtr {
+			srcVar = "*" + srcVar
+		}
+
+		return fmt.Sprintf(convertGoAnyToProtobufAnyFunc, srcVar, srcVar)
+	}
+	
 	tgtType := protoBufNativeGoTypeName(tgt.Type)
 	if srcPtr {
 		srcVar = "*" + srcVar
@@ -658,6 +705,15 @@ func convertPrimitiveToProto(_, tgt *expr.AttributeExpr, srcPtr, _ bool, srcVar
 }
 
 func convertPrimitiveFromProto(_, tgt *expr.AttributeExpr, srcPtr, _ bool, srcVar string, ta *transformAttrs) string {
+	// Special handling for Any type conversion from google.protobuf.Any
+	if tgt.Type.Kind() == expr.AnyKind {
+		if srcPtr {
+			srcVar = "*" + srcVar
+		}
+
+		return fmt.Sprintf(convertProtobufAnyToGoAnyFunc, srcVar, srcVar)
+	}
+	
 	tgtType, _ := codegen.GetMetaType(tgt)
 	if tgtType == "" {
 		tgtType = ta.TargetCtx.Scope.Ref(tgt, ta.TargetCtx.Pkg(tgt))
diff --git a/grpc/codegen/server_types.go b/grpc/codegen/server_types.go
index 49ea51b7..f5818d7a 100644
--- a/grpc/codegen/server_types.go
+++ b/grpc/codegen/server_types.go
@@ -70,6 +70,28 @@ func serverType(genpkg string, svc *expr.GRPCServiceExpr, services *ServicesData
 			{Path: path.Join(genpkg, svcName, "views"), Name: sd.Service.ViewsPkg},
 			{Path: path.Join(genpkg, "grpc", svcName, pbPkgName), Name: sd.PkgName},
 		}
+		// Add imports if Any type is used
+		needsAnyTypeImports := false
+		for _, e := range svc.GRPCEndpoints {
+			if hasAnyType(e.MethodExpr.Payload) || hasAnyType(e.MethodExpr.Result) {
+				needsAnyTypeImports = true
+				break
+			}
+			for _, er := range e.MethodExpr.Errors {
+				if hasAnyType(er.AttributeExpr) {
+					needsAnyTypeImports = true
+					break
+				}
+			}
+			if needsAnyTypeImports {
+				break
+			}
+		}
+		if needsAnyTypeImports {
+			imports = append(imports, &codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/anypb", Name: "anypb"})
+			imports = append(imports, &codegen.ImportSpec{Path: "encoding/json"})
+			imports = append(imports, &codegen.ImportSpec{Path: "google.golang.org/protobuf/types/known/structpb", Name: "structpb"})
+		}
 		imports = append(imports, sd.Service.ProtoImports...)
 		sections = []*codegen.SectionTemplate{codegen.Header(svc.Name()+" gRPC server types", "server", imports)}
 		for _, init := range initData {
diff --git a/grpc/codegen/service_data.go b/grpc/codegen/service_data.go
index 74e138a2..cb257adb 100644
--- a/grpc/codegen/service_data.go
+++ b/grpc/codegen/service_data.go
@@ -662,6 +662,19 @@ func collectMessages(at *expr.AttributeExpr, sd *ServiceData, seen map[string]st
 		}
 	}
 	if expr.IsPrimitive(at.Type) {
+		// Add google.protobuf.Any import when Any type is used
+		if at.Type.Kind() == expr.AnyKind {
+			found := false
+			for _, imp := range imports {
+				if imp == "google/protobuf/any.proto" {
+					found = true
+					break
+				}
+			}
+			if !found {
+				imports = append(imports, "google/protobuf/any.proto")
+			}
+		}
 		return data, imports
 	}
 	collect := func(at *expr.AttributeExpr) ([]*service.UserTypeData, []string) {
@@ -1318,3 +1331,34 @@ func isEmpty(dt expr.DataType) bool {
 	}
 	return false
 }
+
+// hasAnyType recursively checks if the given attribute uses the Any type.
+func hasAnyType(att *expr.AttributeExpr) bool {
+	if att == nil {
+		return false
+	}
+	if att.Type.Kind() == expr.AnyKind {
+		return true
+	}
+	switch dt := att.Type.(type) {
+	case expr.UserType:
+		return hasAnyType(dt.Attribute())
+	case *expr.Object:
+		for _, nat := range *dt {
+			if hasAnyType(nat.Attribute) {
+				return true
+			}
+		}
+	case *expr.Array:
+		return hasAnyType(dt.ElemType)
+	case *expr.Map:
+		return hasAnyType(dt.KeyType) || hasAnyType(dt.ElemType)
+	case *expr.Union:
+		for _, nat := range dt.Values {
+			if hasAnyType(nat.Attribute) {
+				return true
+			}
+		}
+	}
+	return false
+}
diff --git a/grpc/docs/FAQ.md b/grpc/docs/FAQ.md
index ff2eea80..927a004f 100644
--- a/grpc/docs/FAQ.md
+++ b/grpc/docs/FAQ.md
@@ -49,3 +49,55 @@ type ArrayOfBool struct {
   Field []bool
 }
 ```
+
+# How does goa handle the Any type in gRPC?
+
+Goa supports the `Any` type in gRPC by mapping it to `google.protobuf.Any`. The conversion between Go's `any` type and protobuf's `Any` is done using JSON marshaling/unmarshaling.
+
+## Conversion Process
+
+- **Go to Protobuf**: When converting from Go `any` to `*anypb.Any`, the value is JSON-marshaled and wrapped in a `structpb.Value`.
+- **Protobuf to Go**: When converting from `*anypb.Any` to Go `any`, the value is unwrapped and JSON-unmarshaled.
+
+## Example Usage
+
+In your Goa design:
+```go
+Method("echo", func() {
+    Payload(func() {
+        Field(1, "data", Any, "Any type of data")
+    })
+    Result(func() {
+        Field(1, "data", Any, "Echoed data")
+    })
+    GRPC(func() {
+        Response(CodeOK)
+    })
+})
+```
+
+This generates the following protobuf:
+```proto
+import "google/protobuf/any.proto";
+
+message EchoRequest {
+    optional google.protobuf.Any data = 1;
+}
+
+message EchoResponse {
+    optional google.protobuf.Any data = 1;
+}
+```
+
+## Supported Patterns
+
+- Direct Any fields: `Field(1, "data", Any)`
+- Maps with Any values: `MapOf(String, Any)`
+- Arrays of Any: `ArrayOf(Any)`
+- Nested structures containing Any
+
+## Limitations
+
+- The JSON conversion means that complex Go types may not roundtrip perfectly
+- Only JSON-serializable values are supported
+- Type information is lost during conversion



Test patch:
diff --git a/expr/grpc_endpoint_test.go b/expr/grpc_endpoint_test.go
index 35da447f..0adbade7 100644
--- a/expr/grpc_endpoint_test.go
+++ b/expr/grpc_endpoint_test.go
@@ -15,14 +15,8 @@ func TestGRPCEndpointValidation(t *testing.T) {
 		Errors []string
 	}{
 		"endpoint-with-any-type": {
-			DSL: testdata.GRPCEndpointWithAnyType,
-			Errors: []string{`service "Service" gRPC endpoint "Method": Map key type is Any type which is not supported in gRPC
-service "Service" gRPC endpoint "Method": Array element type is Any type which is not supported in gRPC
-service "Service" gRPC endpoint "Method": Attribute "invalid_primitive" is Any type which is not supported in gRPC
-service "Service" gRPC endpoint "Method": Array element type is Any type which is not supported in gRPC
-service "Service" gRPC endpoint "Method": Error "invalid_error_type" type is Any type which is not supported in gRPC
-service "Service" gRPC endpoint "Method": Map element type is Any type which is not supported in gRPC`,
-			},
+			DSL:    testdata.GRPCEndpointWithAnyType,
+			Errors: []string{}, // Any type is now supported in gRPC
 		},
 		"endpoint-with-untagged-fields": {
 			DSL: testdata.GRPCEndpointWithUntaggedFields,
@@ -53,7 +47,7 @@ service "Service" gRPC endpoint "Method": field number 2 in attribute "key_dup_i
 			Errors: []string{
 				`service "Service" method "MethodUnion": union type choice has array elements, not supported by gRPC
 service "Service" method "MethodUnion": union type choice has map elements, not supported by gRPC`,
-				`service "Service" gRPC endpoint "MethodUnion": Attribute "value" is Any type which is not supported in gRPC`,
+				// Any type error removed as Any is now supported
 			},
 		},
 	}
diff --git a/grpc/codegen/protobuf_test.go b/grpc/codegen/protobuf_test.go
index da3d76e0..cbb78546 100644
--- a/grpc/codegen/protobuf_test.go
+++ b/grpc/codegen/protobuf_test.go
@@ -1,6 +1,10 @@
 package codegen
 
-import "testing"
+import (
+	"testing"
+
+	"goa.design/goa/v3/expr"
+)
 
 func TestProtobufify(t *testing.T) {
 	cases := []struct {
@@ -47,3 +51,130 @@ func TestProtobufify(t *testing.T) {
 		})
 	}
 }
+
+func TestProtoNativeType(t *testing.T) {
+	cases := []struct {
+		Name     string
+		DataType expr.DataType
+		Expected string
+	}{{
+		"Boolean", expr.Boolean, "bool",
+	}, {
+		"Int", expr.Int, "sint32",
+	}, {
+		"Int32", expr.Int32, "sint32",
+	}, {
+		"Int64", expr.Int64, "sint64",
+	}, {
+		"UInt", expr.UInt, "uint32",
+	}, {
+		"UInt32", expr.UInt32, "uint32",
+	}, {
+		"UInt64", expr.UInt64, "uint64",
+	}, {
+		"Float32", expr.Float32, "float",
+	}, {
+		"Float64", expr.Float64, "double",
+	}, {
+		"String", expr.String, "string",
+	}, {
+		"Bytes", expr.Bytes, "bytes",
+	}, {
+		"Any", expr.Any, "google.protobuf.Any",
+	}}
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			got := protoNativeType(c.DataType)
+			if got != c.Expected {
+				t.Errorf("got %q, expected %q", got, c.Expected)
+			}
+		})
+	}
+}
+
+func TestProtoBufNativeGoTypeName(t *testing.T) {
+	cases := []struct {
+		Name     string
+		DataType expr.DataType
+		Expected string
+	}{{
+		"Boolean", expr.Boolean, "bool",
+	}, {
+		"Int", expr.Int, "int32",
+	}, {
+		"Int32", expr.Int32, "int32",
+	}, {
+		"Int64", expr.Int64, "int64",
+	}, {
+		"UInt", expr.UInt, "uint32",
+	}, {
+		"UInt32", expr.UInt32, "uint32",
+	}, {
+		"UInt64", expr.UInt64, "uint64",
+	}, {
+		"Float32", expr.Float32, "float32",
+	}, {
+		"Float64", expr.Float64, "float64",
+	}, {
+		"String", expr.String, "string",
+	}, {
+		"Bytes", expr.Bytes, "[]byte",
+	}, {
+		"Any", expr.Any, "*anypb.Any",
+	}}
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			got := protoBufNativeGoTypeName(c.DataType)
+			if got != c.Expected {
+				t.Errorf("got %q, expected %q", got, c.Expected)
+			}
+		})
+	}
+}
+
+func TestHasAnyType(t *testing.T) {
+	cases := []struct {
+		Name     string
+		Attr     *expr.AttributeExpr
+		Expected bool
+	}{{
+		"NoAnyType", &expr.AttributeExpr{Type: expr.String}, false,
+	}, {
+		"DirectAnyType", &expr.AttributeExpr{Type: expr.Any}, true,
+	}, {
+		"ArrayWithAnyType", &expr.AttributeExpr{
+			Type: &expr.Array{ElemType: &expr.AttributeExpr{Type: expr.Any}},
+		}, true,
+	}, {
+		"MapWithAnyKeyType", &expr.AttributeExpr{
+			Type: &expr.Map{
+				KeyType:  &expr.AttributeExpr{Type: expr.Any},
+				ElemType: &expr.AttributeExpr{Type: expr.String},
+			},
+		}, true,
+	}, {
+		"MapWithAnyElemType", &expr.AttributeExpr{
+			Type: &expr.Map{
+				KeyType:  &expr.AttributeExpr{Type: expr.String},
+				ElemType: &expr.AttributeExpr{Type: expr.Any},
+			},
+		}, true,
+	}, {
+		"ObjectWithAnyField", &expr.AttributeExpr{
+			Type: &expr.Object{
+				&expr.NamedAttributeExpr{
+					Name:      "field",
+					Attribute: &expr.AttributeExpr{Type: expr.Any},
+				},
+			},
+		}, true,
+	}}
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			got := hasAnyType(c.Attr)
+			if got != c.Expected {
+				t.Errorf("got %t, expected %t", got, c.Expected)
+			}
+		})
+	}
+}
diff --git a/grpc/codegen/protobuf_transform_test.go b/grpc/codegen/protobuf_transform_test.go
index a6f01f96..c5cf6b8c 100644
--- a/grpc/codegen/protobuf_transform_test.go
+++ b/grpc/codegen/protobuf_transform_test.go
@@ -184,6 +184,58 @@ func TestProtoBufTransform(t *testing.T) {
 	}
 }
 
+func TestProtoBufTransformAnyType(t *testing.T) {
+	var (
+		sd     = &ServiceData{Name: "Service", Scope: codegen.NewNameScope()}
+		svcCtx = codegen.NewAttributeContext(false, false, true, "", sd.Scope)
+		pbCtx  = protoBufTypeContext("", sd.Scope, false)
+	)
+
+	cases := []struct {
+		Name    string
+		ToProto bool
+		NewVar  bool
+		Ctx     *codegen.AttributeContext
+	}{
+		{"any-to-proto-new-var", true, true, svcCtx},
+		{"proto-to-any-new-var", false, true, svcCtx},
+		{"any-to-proto-assign-var", true, false, svcCtx},
+		{"proto-to-any-assign-var", false, false, svcCtx},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			source := &expr.AttributeExpr{Type: expr.Any}
+			target := &expr.AttributeExpr{Type: expr.Any}
+			srcCtx := c.Ctx
+			tgtCtx := c.Ctx
+			if c.ToProto {
+				tgtCtx = pbCtx
+			} else {
+				srcCtx = pbCtx
+			}
+			code, _, err := protoBufTransform(source, target, "source", "target", srcCtx, tgtCtx, c.ToProto, c.NewVar)
+			require.NoError(t, err)
+			t.Logf("Generated code: %s", code)
+
+			// Check if transformation contains Any type conversion logic
+			if c.ToProto {
+				require.Contains(t, code, "func() *anypb.Any", "To proto conversion should generate Any type conversion function")
+			} else {
+				require.Contains(t, code, "func() any", "From proto conversion should generate any type conversion function")
+			}
+
+			// Check the assignment operator used based on newVar parameter
+			if c.NewVar {
+				require.Contains(t, code, "target :=", "New variable should use := operator")
+			} else {
+				require.Contains(t, code, "target =", "Assignment should use = operator")
+				require.NotContains(t, code, "target :=", "Assignment should not use := operator")
+			}
+		})
+	}
+}
+
 func pointerContext(pkg string, scope *codegen.NameScope) *codegen.AttributeContext {
 	return codegen.NewAttributeContext(true, false, true, pkg, scope)
 }
diff --git a/grpc/codegen/testdata/golden/protobuf_type_encode_to-protobuf-type_optional-to-optional.go.golden b/grpc/codegen/testdata/golden/protobuf_type_encode_to-protobuf-type_optional-to-optional.go.golden
index 74804198..d49ccc1a 100644
--- a/grpc/codegen/testdata/golden/protobuf_type_encode_to-protobuf-type_optional-to-optional.go.golden
+++ b/grpc/codegen/testdata/golden/protobuf_type_encode_to-protobuf-type_optional-to-optional.go.golden
@@ -3,7 +3,20 @@ func transform() {
 		Float_:  source.Float,
 		String_: source.String,
 		Bytes_:  source.Bytes,
-		Any:     source.Any,
+		Any: func() *anypb.Any {
+			if source.Any == nil {
+				return nil
+			}
+			// Convert Go any to protobuf Any using JSON marshaling
+			if jsonData, err := json.Marshal(source.Any); err == nil {
+				if data, err := anypb.New(&structpb.Value{
+					Kind: &structpb.Value_StringValue{StringValue: string(jsonData)},
+				}); err == nil {
+					return data
+				}
+			}
+			return nil
+		}(),
 	}
 	if source.Int != nil {
 		int_ := int32(*source.Int)
diff --git a/grpc/codegen/testdata/golden/protobuf_type_encode_to-service-type_optional-to-optional.go.golden b/grpc/codegen/testdata/golden/protobuf_type_encode_to-service-type_optional-to-optional.go.golden
index 0550f9b1..5fe84bc2 100644
--- a/grpc/codegen/testdata/golden/protobuf_type_encode_to-service-type_optional-to-optional.go.golden
+++ b/grpc/codegen/testdata/golden/protobuf_type_encode_to-service-type_optional-to-optional.go.golden
@@ -3,7 +3,20 @@ func transform() {
 		Float:  source.Float_,
 		String: source.String_,
 		Bytes:  source.Bytes_,
-		Any:    source.Any,
+		Any: func() any {
+			if source.Any != nil {
+				var value structpb.Value
+				if err := source.Any.UnmarshalTo(&value); err == nil {
+					if str := value.GetStringValue(); str != "" {
+						var result any
+						if err := json.Unmarshal([]byte(str), &result); err == nil {
+							return result
+						}
+					}
+				}
+			}
+			return nil
+		}(),
 	}
 	if source.Int != nil {
 		int_ := int(*source.Int)