The Any type in Protocol Buffers allows you to embed any other Protobuf message type within a single field, effectively turning a strongly-typed system into a dynamically-typed one at runtime.
Let’s see this in action. Imagine you have a system that needs to send different types of events, like UserCreatedEvent and OrderPlacedEvent. Without Any, you’d need a union type or separate fields, which gets messy fast. With Any, you can put them all in one list.
Here’s a simplified Go example:
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
// Assume these are defined in your .proto files
// message UserCreatedEvent { string user_id = 1; }
// message OrderPlacedEvent { string order_id = 1; string user_id = 2; }
func main() {
userEvent := &UserCreatedEvent{UserId: "user-123"}
orderEvent := &OrderPlacedEvent{OrderId: "order-abc", UserId: "user-123"}
// Wrap the user event in an Any
anyUserEvent, err := anypb.New(userEvent)
if err != nil {
log.Fatalf("Failed to marshal user event to Any: %v", err)
}
// Wrap the order event in an Any
anyOrderEvent, err := anypb.New(orderEvent)
if err != nil {
log.Fatalf("Failed to marshal order event to Any: %v", err)
}
// Imagine these are sent over a network or stored
eventList := []*anypb.Any{anyUserEvent, anyOrderEvent}
// Later, when receiving or processing:
for i, anyMsg := range eventList {
fmt.Printf("Processing event %d:\n", i)
// To get the original message back, we need to unpack it.
// We need to know *what* type to expect, or try to guess.
// In a real system, you'd have a registry or context.
// Example 1: Unpack as UserCreatedEvent
var userEvent UserCreatedEvent
if err := anyMsg.UnmarshalTo(&userEvent); err == nil {
fmt.Printf(" It's a UserCreatedEvent! User ID: %s\n", userEvent.UserId)
continue // Move to the next event
}
// Example 2: Unpack as OrderPlacedEvent
var orderEvent OrderPlacedEvent
if err := anyMsg.UnmarshalTo(&orderEvent); err == nil {
fmt.Printf(" It's an OrderPlacedEvent! Order ID: %s, User ID: %s\n", orderEvent.OrderId, orderEvent.UserId)
continue // Move to the next event
}
// Fallback if we don't know how to handle it
fmt.Printf(" Unknown event type (Type URL: %s)\n", anyMsg.TypeUrl)
}
}
The core idea is that anypb.New(message) takes any proto.Message, serializes it, and stores the serialized bytes along with the message’s fully qualified type name (its "Type URL"). When you receive an Any message, you use anypb.Any.UnmarshalTo(targetMessage) to deserialize the bytes back into a concrete Protobuf message type. The targetMessage you pass in (e.g., &UserCreatedEvent{}) tells UnmarshalTo what type to attempt to deserialize into. If the Type URL inside the Any matches the expected type for the targetMessage, and the bytes are valid for that type, it succeeds.
This pattern is incredibly useful for:
- API Versioning: An API can return a base
Anyfield that clients can unpack into a specific version of a message. - Event Buses: As shown, a single event channel can carry diverse event types.
- Configuration: Storing different kinds of plugin configurations or feature flags.
- Extensibility: Allowing third-party services to define and send their own message types within your system’s framework.
The TypeUrl is crucial; it’s a string in the format type.googleapis.com/<package>.<MessageName>. For example, a UserCreatedEvent from my.package would have a Type URL like type.googleapis.com/my.package.UserCreatedEvent. This URL is what enables UnmarshalTo to know which Protobuf descriptor to use for deserialization.
The most surprising aspect is how Any achieves dynamic typing without sacrificing Protobuf’s core performance and schema validation benefits. It doesn’t magically make any arbitrary Go struct work; it only works with other valid Protobuf messages. The schema for the embedded message must still be known to the receiver at the time of unpacking. The Any type acts as a standardized wrapper, not a bypass for schema definition. You don’t get runtime type reflection in the way you might with, say, JSON. You still need to know what you might receive and have the corresponding .proto definitions available to generate the Go types for unpacking.
The system in action relies heavily on the anypb.New() and anypb.Any.UnmarshalTo() functions. The former serializes a known Protobuf message into a byte slice and prefixes it with its type’s URL. The latter takes that byte slice and type URL, looks up the schema associated with the URL (which requires having the .proto definitions compiled into your project), and attempts to deserialize the bytes into the provided target message variable.
When you’re working with Any, you’re essentially dealing with a tuple: (type_url: string, value: bytes). The anypb package handles the serialization and deserialization of the value bytes according to the schema indicated by type_url. This means you need to have the Protobuf compiler (protoc) and the appropriate plugins available to generate Go code from your .proto files, both for the messages you’re embedding and for the google.golang.org/protobuf/types/known/anypb package itself.
The mental model to build is that Any is a serialization format, not a runtime type system. It’s a way to bundle a message’s serialized form with its identity. The receiver must possess the definition for that identity to interpret the payload. You control the behavior by defining the set of message types that can be legitimately embedded and by implementing the logic to unpack and handle each of those types at the receiving end. The type.googleapis.com/ prefix is a convention, and the part after it is the fully qualified name of the message type as defined in your .proto files, including its package.
The one thing most people don’t realize is that the type_url isn’t just descriptive; it’s the key to Protobuf’s descriptor lookup. When UnmarshalTo is called, it uses the type_url to find the registered Protobuf descriptor for that specific message type within the running program. If the descriptor isn’t found (because the .proto file wasn’t compiled into the binary, or the Go types weren’t generated), UnmarshalTo will fail, even if the bytes are technically valid for some message. This is why Any doesn’t break type safety; it just defers the type resolution to runtime based on a string identifier that maps back to compile-time generated code.
The next concept you’ll likely encounter is managing the registry of known types, especially in larger systems where not all possible embedded message types might be compiled into every service.