We Prefer Extensions Over Any
When proto3 was introduced, it removed general-purpose
extensions
(keeping them only for custom options) and introduced
google.protobuf.Any
as their replacement.
Unfortunately, Any has enough design flaws that that this is a decision the
Protobuf team regrets. We have no plans to formally deprecate Any, and it
remains supported, but we recommend using extensions instead of Any for nearly
all use cases.
- Protobuf Editions restored full support for extensions starting in Edition 2023.
- For new schemas, prefer extensions over
Anywhenever extensions can satisfy the use case (see Proto Best Practices). - Reserve
Anyfor the uncommon case where horizontal infrastructure needs to propagate completely arbitrary messages and declaring an extension for each possible type is infeasible.
What Extensions and Any Are
Both mechanisms solve the same dependency problem: letting a container message
carry a submessage whose .proto file the container does not import. Instead of
the container depending on every possible payload type, each payload depends on
the container.
Extensions
An extension
is a field defined outside of its container message, using a field number from
an extensions range reserved by the container:
// media/user_content.proto
edition = "2024";
package media;
message UserContent {
string id = 1;
extensions 100 to 199 [
declaration = {
number: 100,
full_name: ".kittens.kitten_video",
type: ".kittens.Video"
}
];
}
// kittens/video_ext.proto
edition = "2024";
package kittens;
import "kittens/video.proto";
import "media/user_content.proto";
extend media.UserContent {
Video kitten_video = 100;
}
In generated code, the extension is accessed as a typed submessage stored directly on the parent message:
media::UserContent content;
content.MutableExtension(kittens::kitten_video)->set_title("Cute kittens");
if (content.HasExtension(kittens::kitten_video)) {
const kittens::Video& video = content.GetExtension(kittens::kitten_video);
}
On the wire, an extension is identical to a regular field with the same field
number, type, and cardinality. With extension
declarations,
the container can list the number, name, and type name of each extension in one
place without importing the payload .proto file.
google.protobuf.Any
google.protobuf.Any
is a wrapper message defined in google/protobuf/any.proto:
syntax = "proto3";
package google.protobuf;
message Any {
string type_url = 1;
bytes value = 2;
}
Instead of extending the container message, the container declares a field of
type google.protobuf.Any. Packing a message into Any serializes the inner
message into value and writes a URI containing the inner message’s fully
qualified name into type_url:
media::UserContent content;
kittens::Video video;
video.set_title("Cute kittens");
content.mutable_payload()->PackFrom(video);
if (content.payload().Is<kittens::Video>()) {
kittens::Video unpacked;
if (content.payload().UnpackTo(&unpacked)) {
// use unpacked
}
}
Why Proto3 Replaced Extensions with Any
In proto2, extensions had two real problems:
- There was no built-in way to coordinate extension numbers on a container
message without importing the extensions. Because
extendblocks lived in separate.protofiles, two teams could pick the same field number on a shared container message and silently collide on the wire. - Proto3 tried to simplify the language so that new runtimes could represent
messages as plain open
structs with minimal
runtime machinery. Extensions required an
ExtensionRegistry, extension storage on messages, and parameterized accessor APIs in every language.
Any avoided both: on the binary wire it is an ordinary message with a string
and a bytes field, its discriminator is the globally unique message name
rather than a coordinated field number, and runtime support could be limited to
PackFrom / UnpackTo helpers plus special-case handling in
ProtoJSON and TextFormat.
The open-struct premise was abandoned (see We Prefer Opaque APIs).
Risks of extension number collisions are still a concern with extensions, but can be mitigated in most cases by using extension declarations.
Why We Regret Any as a Replacement for Extensions
Type Names on the Binary Wire Prevent Renaming and Repackaging
A core property of the Protobuf binary wire format is that message names, field
names, and package names do not appear on the wire. Because the binary format
identifies fields by numeric tag, you can rename a field, rename a message, or
move a message to a different .proto package without breaking wire
compatibility with deployed binaries or stored data.
Any breaks this property. Because PackFrom() writes the fully qualified
message name into type_url on the binary wire, any message packed into an
Any cannot safely be renamed or moved to a different .proto package:
- If a message is renamed or moved to a new package,
Is<T>()andUnpackTo()compare the new descriptor’sfull_name()against the oldtype_urlstring and reject existing payloads, even though the serialized bytes invalueare wire-compatible. - At the same time, new binaries start writing the new
type_urlstring, which older binaries and rollback targets do not recognize.
Worse, Any is not opt-in. A message author does not declare that their
message may be packed into an Any. Any caller in another repository or service
can pass an arbitrary message to PackFrom() without the message owner’s
knowledge, and because packing can happen through generic or reflective code
paths, static analysis cannot prove that a message type is never stored in an
Any. This is a very spooky property that makes it difficult to know if
renaming a message might not be safe.
Extensions identify fields on the wire by their numeric tag, just like regular
fields. Renaming an extension, renaming the message type it holds, or moving
either across .proto packages is safe on the binary wire.
Wire Incompatibility with Regular Fields
An extension has the exact same binary wire encoding as a regular field with the same field number, type, and cardinality. That makes the boundary between regular fields and extensions reversible:
- If a regular field on a container message is causing unwanted build
dependencies, it can be moved out into an
extendblock with the same field number and type without changing a single byte on the wire. - Conversely, a widely used extension can be promoted directly into a regular field on the container message.
Any wraps its payload inside a submessage containing type_url (field 1) and
value (field 2). It is wire-incompatible with a normal submessage field, so a
field cannot be migrated into or out of an Any without breaking the wire
format.
Wire Size Overhead
An extension tag on the wire is a single varint encoding the field number and wire type: 2 bytes for field numbers up to 2047, and at most 5 bytes for the largest valid field numbers.
Every Any message carries the full type.googleapis.com/package.MessageName
string on the wire in addition to the nested length prefixes. A typical
type_url adds 40 to 80 bytes per instance. For small messages or repeated google.protobuf.Any fields, the repeated type_url strings often take more
space than the payload itself.
In-Memory Cost and Eager Serialization
Because Any stores its payload as a bytes value field rather than as a typed
submessage in the message tree, working with Any in memory is clumsy and
expensive:
PackFrom()immediately serializes the inner message into abytesbuffer, even if the outer message is only passed around in memory.- In most languages (including C++, Go, Python, and open-source Java), calling
UnpackTo()allocates a separate message object and parsesvaluefrom scratch on every read, unless the caller maintains a separate cache by hand. By contrast, an extension is stored directly inside the parent message (and shares its arena in C++), so readingGetExtension()is a pointer lookup.
A common misconception is that Any is cheaper for intermediate proxies that do
not care about the payload because value stays unparsed until UnpackTo() is
called. Extensions already have this property: if a binary does not link in or
register an extension, the bytes remain unparsed in the unknown field set and
are forwarded intact on serialization. Even when a binary does link in the
extension, runtimes that support lazy fields defer parsing the extension until
the first GetExtension() call and then cache the parsed submessage in place.
Broken MergeFrom, Equality, and Reflection Semantics
To Protobuf, an Any is just a two-field message (string type_url = 1; bytes value = 2;), not a typed container. Standard message operations act on those
two scalar fields rather than on the packed message inside:
MergeFrom
When two messages with a submessage extension are merged via MergeFrom(), the
extension submessages are merged recursively field by field, and distinct
extensions on the same container coexist.
When two Any messages are merged, MergeFrom() merges type_url and value
as ordinary scalar fields, overwriting value instead of merging the inner
message. Worse, because google/protobuf/any.proto is defined with syntax = "proto3", both type_url and value have implicit
presence:
- Suppose
Ais anAnypacked with a non-emptyBarmessage (type_url = "type.googleapis.com/Bar",value = "\x08\x01"). - Suppose
Bis anAnypacked with a default, emptyQuuxmessage. Because an all-default message serializes to 0 bytes,Bhastype_url = "type.googleapis.com/Quux"andvalue = "". - Calling
A.MergeFrom(B)overwritesA.type_urlwith"type.googleapis.com/Quux", but becauseB.valueis empty (""), implicit presence treatsB.valueas unset and does not overwriteA.value.
The merged Any now has Quux’s type_url paired with Bar’s serialized wire
bytes.
Equality
Standard message equality on Any compares the raw value byte string rather
than the logical inner message. As explained in Serialization Is Not
Canonical,
two logically equal Protobuf messages can serialize to different byte sequences
(for example, due to map entry ordering or unknown fields), causing two Any
messages holding equivalent payloads to compare unequal.
Reflection and Generic Traversal
Unless a tool links in the packed message’s descriptor and writes custom
Any-unpacking code, reflection walkers, field-path utilities, and redaction
passes only see a string type_url and an opaque bytes value. Extensions have
normal FieldDescriptor entries and are visited by standard reflection walkers
without special cases.
Poor Fit for Mobile and Lite Runtimes
In mobile, embedded, and web client builds (LITE_RUNTIME), Protobuf strips
descriptors and symbol names to reduce binary size and avoid leaking unreleased
feature names in shipped client binaries.
Any relies on runtime type names to construct and check type_url:
- In JavaLite,
MessageLitedoes not carry descriptor names, soAny.pack(),Any.unpack(), andAny.is()are omitted from the generatedAnyclass altogether. Callers must construct and comparetype_urlstrings manually. - In C++ Lite,
Any::PackFrom<T>()andUnpackTo<T>()only work because the code generator emits aFullMessageName()string literal on every message class, which forces the binary to retain the.protopackage and message name for every type used withAny.
Extensions use numeric tags and generated extension identifiers, which work in
LITE_RUNTIME builds and support tree shaking without retaining descriptor
strings.
Type URLs Do Not Provide Dynamic Schema Discovery
The type_url field was originally formatted as a URL
(type.googleapis.com/...) on the theory that runtimes might fetch unknown
schemas over HTTP at parse time. Protobuf never implemented remote type fetching
and explicitly warns against contacting type_url endpoints in any.proto as a
security problem. Every parser simply strips everything up to the last /, and
unpacking an Any in JSON, TextFormat, or reflection still requires the target
descriptor to be linked or loaded into the local binary’s descriptor pool, just
like an extension.
What to Use Instead
Regular Submessage Fields or oneof
When the set of possible payload types is bounded, known to the container
author, and importing them directly into the container .proto does not cause
dependency problems, use a regular submessage field or a oneof. A oneof
makes the set of alternatives explicit in one place and generates the cleanest
API in every language.
Extensions with Extension Declarations
When payload definitions need to live in separate .proto files so the
container does not depend on them, use
extensions:
- Add extension
declarations
to the container’s
extensionsrange so all allocated extension numbers and type names are listed in one place and checked byprotocwithout importing the payload.protofiles. - If central code-review ownership on the container
.protofile would be a bottleneck for teams adding extensions, put the extendable container (or a dedicated extension-carrier submessage) in a separate.protofile with broader ownership. - If your container message is currently in a
syntax = "proto3"file (which does not allowextensionsranges), migrate that file to Protobuf Editions. Moving a file fromproto3to Edition 2023 is wire-compatible, and remainingproto3files can still import it.
When Any Is Still Appropriate
The one case where Any is still the right tool is unbounded horizontal
infrastructure: systems that transport, log, or store completely arbitrary
messages across independent domains (for example, a generic RPC error detail
list like google.rpc.Status.details, a generic task queue payload, or a dead
letter queue) where declaring an extension on the envelope for every message
type in the ecosystem is infeasible.
Whenever you do use Any, treat the package name and message name of every
packed message as a permanently frozen part of your wire protocol.