Documentation
¶
Overview ¶
Package dynamicpb provides a dynamic protobuf message implementation that constructs messages from descriptors at runtime without generated code. It enables creating, populating, serializing, and deserializing protobuf messages when the message schema is only known at runtime, such as when processing descriptors obtained from a protobuf registry or a gRPC reflection server.
The core type is Message, a concrete struct that implements both protoreflect.Message (12 reflection methods including Get, Set, Has, Clear, Range, and WhichOneof) and proto.Message (9 embedded interfaces including MarshalAppender, Unmarshaler, Merger, Cloner, and Validator). This dual conformance gives dynamic messages full interoperability with the proto ecosystem: they can be marshaled, unmarshaled, merged, cloned, validated, and reflected upon using the same interfaces as generated message types.
Two container types support composite fields:
List is a repeated field container implementing protoreflect.List. It provides indexed access to repeated elements with Get, Set, Append, and Truncate, following Go slice semantics.
Map is a map field container implementing protoreflect.Map. It provides key-based access to map entries with Get, Set, Has, Clear, and Range, following Go map semantics with undefined iteration order.
The New function is the primary constructor. It accepts any MessageDescriptorAccessor and returns a fully initialized Message ready for use:
msg := dynamicpb.New(messageDescriptor)
msg.Set(fieldDesc, protoreflect.ValueOfString("hello"))
data, err := msg.Marshal()
NewList and NewMap construct standalone containers for repeated and map fields respectively.
Primary use cases include gRPC reflection servers that process messages without compile-time type knowledge, schema-driven middleware that inspects and transforms messages in transit, protobuf registries that store and retrieve arbitrary message types, and serialization pipelines that decode wire data, inspect or transform fields, and re-encode without losing information.
Field values are stored as protoreflect.Value entries in a map keyed by field number, providing O(1) field access for Get, Set, Has, and Clear. The constructor pre-builds a lookup table mapping field numbers to their FieldDescriptorAccessor, enabling O(1) dispatch during decode without repeated descriptor iteration. The values map and oneof tracking map are lazily initialized on first Set to avoid allocation for messages that are only read, such as default value sentinels returned by Get on unset message fields.
The package follows a split error convention. Programmer errors such as passing a field descriptor from a different message to Get, Set, Has, or Clear, or passing a nil descriptor to New, cause panics prefixed with "dynamicpb: " for easy identification (e.g., "dynamicpb: field \"name\" does not belong to message \"example.Foo\""). Data-driven failures return errors: Unmarshal returns errors from the decode package for malformed wire data, Merge returns an error when the source and destination have incompatible descriptors, and Validate returns an error listing missing required fields or invalid closed enum values.
This package does not provide thread safety. Dynamic messages are not safe for concurrent mutation, matching the semantics of Go maps and slices. Extension fields and well-known type special handling (Any, Timestamp, Duration, Struct) are out of scope and planned for future work.
This package depends on descriptor, protoreflect, proto, encode, decode, scalar, and wire from this module, plus fmt, math, sort, and errors from the standard library. It has zero external dependencies.
message.go defines the Message struct and its constructors for dynamic protobuf messages. Field lookup tables, oneof tracking, and container factory helpers live here; the protocol-specific methods (marshal, unmarshal, reflect, merge/clone) are in their respective split files. The init()-based registration of newMessageFunc is in init_gc.go; TinyGo consumers should call Register() from register.go instead.
message_fields.go implements Merge, Clone, Reset, Validate, and their supporting deep-copy and validation helpers for dynamic messages.
message_marshal.go implements Size, MarshalAppend, and Marshal for dynamic messages, along with the scalar-to-uint64 conversion helpers used by both size computation and wire encoding.
message_reflect.go implements the protoreflect.Message interface methods for dynamic messages: Descriptor, New, Interface, ProtoReflect, IsValid, Get, Set, Has, Clear, Range, WhichOneof, GetUnknown, and SetUnknown.
message_unmarshal.go implements Unmarshal and its supporting decode methods for dynamic messages, handling singular, repeated, packed, and map field decoding from wire-format bytes.
Index ¶
- Variables
- func CheckRequiredFields(msg protoreflect.Message) error
- func Register()
- func ValidateStringUTF8(fd descriptor.FieldDescriptorAccessor, s string) error
- type LazyMessage
- func (lm *LazyMessage) Clear(fd descriptor.FieldDescriptorAccessor)
- func (lm *LazyMessage) Clone() proto.Message
- func (lm *LazyMessage) Descriptor() descriptor.MessageDescriptorAccessor
- func (lm *LazyMessage) Get(fd descriptor.FieldDescriptorAccessor) protoreflect.Value
- func (lm *LazyMessage) GetUnknown() []byte
- func (lm *LazyMessage) Has(fd descriptor.FieldDescriptorAccessor) bool
- func (lm *LazyMessage) Interface() protoreflect.ProtoMessage
- func (lm *LazyMessage) IsValid() bool
- func (lm *LazyMessage) Marshal() ([]byte, error)
- func (lm *LazyMessage) MarshalAppend(b []byte) ([]byte, error)
- func (lm *LazyMessage) Merge(src proto.Message) error
- func (lm *LazyMessage) New() protoreflect.Message
- func (lm *LazyMessage) ProtoReflect() protoreflect.Message
- func (lm *LazyMessage) Range(f func(descriptor.FieldDescriptorAccessor, protoreflect.Value) bool)
- func (lm *LazyMessage) Reset()
- func (lm *LazyMessage) Set(fd descriptor.FieldDescriptorAccessor, v protoreflect.Value)
- func (lm *LazyMessage) SetUnknown(b []byte)
- func (lm *LazyMessage) Size() int
- func (lm *LazyMessage) Unmarshal(b []byte) error
- func (lm *LazyMessage) Validate() error
- func (lm *LazyMessage) WhichOneof(od descriptor.OneofDescriptorAccessor) descriptor.FieldDescriptorAccessor
- type List
- type Map
- func (m *Map) Clear(k protoreflect.MapKey)
- func (m *Map) Get(k protoreflect.MapKey) protoreflect.Value
- func (m *Map) Has(k protoreflect.MapKey) bool
- func (m *Map) IsValid() bool
- func (m *Map) Len() int
- func (m *Map) NewValue() protoreflect.Value
- func (m *Map) Range(f func(protoreflect.MapKey, protoreflect.Value) bool)
- func (m *Map) Set(k protoreflect.MapKey, v protoreflect.Value)
- type MarshalOptions
- type Message
- func (m *Message) Clear(fd descriptor.FieldDescriptorAccessor)
- func (m *Message) Clone() proto.Message
- func (m *Message) Descriptor() descriptor.MessageDescriptorAccessor
- func (m *Message) Get(fd descriptor.FieldDescriptorAccessor) protoreflect.Value
- func (m *Message) GetUnknown() []byte
- func (m *Message) Has(fd descriptor.FieldDescriptorAccessor) bool
- func (m *Message) Interface() protoreflect.ProtoMessage
- func (m *Message) IsValid() bool
- func (m *Message) Marshal() ([]byte, error)
- func (m *Message) MarshalAppend(b []byte) ([]byte, error)
- func (m *Message) Merge(src proto.Message) error
- func (m *Message) New() protoreflect.Message
- func (m *Message) ProtoReflect() protoreflect.Message
- func (m *Message) Range(f func(descriptor.FieldDescriptorAccessor, protoreflect.Value) bool)
- func (m *Message) Reset()
- func (m *Message) Set(fd descriptor.FieldDescriptorAccessor, v protoreflect.Value)
- func (m *Message) SetUnknown(b []byte)
- func (m *Message) Size() int
- func (m *Message) Unmarshal(b []byte) error
- func (m *Message) Validate() error
- func (m *Message) WhichOneof(od descriptor.OneofDescriptorAccessor) descriptor.FieldDescriptorAccessor
- type UnmarshalOptions
Constants ¶
This section is empty.
Variables ¶
var ErrExtensionOutOfRange = errors.New("dynamicpb: extension field number outside declared range")
ErrExtensionOutOfRange is returned when an extension field number falls outside the declared extension ranges of the target message during dynamic unmarshal. Use errors.Is to match this sentinel; the wrapped error message includes the field number and message name.
var ErrInvalidUTF8 = errors.New("dynamicpb: string field contains invalid UTF-8")
ErrInvalidUTF8 is returned when a proto3 string field contains bytes that are not valid UTF-8 during Marshal or Unmarshal. Use errors.Is to match this sentinel; the wrapped error message includes the field's full name.
var ErrRecursionLimitExceeded = errors.New("dynamicpb: recursion limit exceeded")
ErrRecursionLimitExceeded is returned when the nesting depth of binary message fields exceeds the configured RecursionLimit during unmarshal. Use errors.Is to match this sentinel.
var ErrRequiredNotSet = errors.New("dynamicpb: required field not set")
ErrRequiredNotSet is returned when one or more proto2 required fields are missing during Marshal, Unmarshal, or Validate. Use errors.Is to match this sentinel; the wrapped error message includes the missing field names.
Functions ¶
func CheckRequiredFields ¶
func CheckRequiredFields(msg protoreflect.Message) error
CheckRequiredFields iterates over the message descriptor's fields and returns ErrRequiredNotSet listing all proto2 required fields that are not present in msg. It returns nil if no required fields are missing. This function works with any protoreflect.Message implementation, not just *Message.
func Register ¶
func Register()
Register sets the package-level newMessageFunc so that List and Map can create message-typed elements without a direct dependency on New. Under standard Go this is handled by init(); under TinyGo consumers must call Register() explicitly before using List or Map with message-typed values.
func ValidateStringUTF8 ¶
func ValidateStringUTF8(fd descriptor.FieldDescriptorAccessor, s string) error
ValidateStringUTF8 checks whether s is valid UTF-8 when the field requires it. Proto3 fields always require UTF-8 validation. Editions fields use the resolved Utf8Validation feature: VERIFY requires validation, NONE skips it. Proto2 fields skip validation entirely. This function is exported so that cross-codec marshal paths (jsoncodec, textcodec) can enforce the same UTF-8 invariant as the binary codec.
Types ¶
type LazyMessage ¶
type LazyMessage struct {
// contains filtered or unexported fields
}
LazyMessage is a protoreflect.Message that defers decoding of wire bytes until first field access. It wraps a message descriptor and raw wire bytes, resolving them into a full *Message via sync.Once on first read or write. LazyMessage is safe for concurrent reads from multiple goroutines.
Fields are ordered with pointer-containing fields first (desc, inner, raw), then non-pointer fields (once, dirty) to minimize GC pointer scanning overhead and padding.
func NewLazyMessage ¶
func NewLazyMessage(desc descriptor.MessageDescriptorAccessor, raw []byte) *LazyMessage
NewLazyMessage creates a LazyMessage from a descriptor and raw wire bytes. The raw bytes are defensively copied to prevent aliasing.
func (*LazyMessage) Clear ¶
func (lm *LazyMessage) Clear(fd descriptor.FieldDescriptorAccessor)
Clear removes a field. It triggers resolution, marks the message as dirty, and delegates to the inner message.
func (*LazyMessage) Clone ¶
func (lm *LazyMessage) Clone() proto.Message
Clone returns a deep copy of the message. If the message has not been resolved, it returns a new LazyMessage sharing the same raw bytes via defensive copy, avoiding a full parse.
func (*LazyMessage) Descriptor ¶
func (lm *LazyMessage) Descriptor() descriptor.MessageDescriptorAccessor
Descriptor returns the message descriptor without triggering resolution.
func (*LazyMessage) Get ¶
func (lm *LazyMessage) Get(fd descriptor.FieldDescriptorAccessor) protoreflect.Value
Get reads a field value by its descriptor. It triggers resolution before delegating to the inner message.
func (*LazyMessage) GetUnknown ¶
func (lm *LazyMessage) GetUnknown() []byte
GetUnknown returns the raw bytes of unknown fields. It triggers resolution before delegating to the inner message.
func (*LazyMessage) Has ¶
func (lm *LazyMessage) Has(fd descriptor.FieldDescriptorAccessor) bool
Has reports whether a field is populated. It triggers resolution before delegating to the inner message.
func (*LazyMessage) Interface ¶
func (lm *LazyMessage) Interface() protoreflect.ProtoMessage
Interface returns the LazyMessage itself as a ProtoMessage without triggering resolution.
func (*LazyMessage) IsValid ¶
func (lm *LazyMessage) IsValid() bool
IsValid reports whether the message was properly constructed. It triggers resolution before delegating to the inner message.
func (*LazyMessage) Marshal ¶
func (lm *LazyMessage) Marshal() ([]byte, error)
Marshal returns the wire-format encoding of the message.
func (*LazyMessage) MarshalAppend ¶
func (lm *LazyMessage) MarshalAppend(b []byte) ([]byte, error)
MarshalAppend appends the wire-format encoding to b. If the message has not been accessed or modified, it returns the original raw bytes without parsing (zero-cost fast path).
func (*LazyMessage) Merge ¶
func (lm *LazyMessage) Merge(src proto.Message) error
Merge merges the fields of src into this message. It resolves both the receiver and, if src is a *LazyMessage, the source before delegating to the inner *Message's Merge method.
func (*LazyMessage) New ¶
func (lm *LazyMessage) New() protoreflect.Message
New returns a fresh empty *Message with the same descriptor without triggering resolution.
func (*LazyMessage) ProtoReflect ¶
func (lm *LazyMessage) ProtoReflect() protoreflect.Message
ProtoReflect returns the LazyMessage as a protoreflect.Message, bridging the proto.Message and protoreflect.Message interface hierarchies.
func (*LazyMessage) Range ¶
func (lm *LazyMessage) Range(f func(descriptor.FieldDescriptorAccessor, protoreflect.Value) bool)
Range iterates over every populated field. It triggers resolution before delegating to the inner message.
func (*LazyMessage) Reset ¶
func (lm *LazyMessage) Reset()
Reset clears all state, returning the message to its zero state. It clears the raw bytes, inner message, dirty flag, and replaces the sync.Once so subsequent operations start fresh.
func (*LazyMessage) Set ¶
func (lm *LazyMessage) Set(fd descriptor.FieldDescriptorAccessor, v protoreflect.Value)
Set writes a field value. It triggers resolution, marks the message as dirty, and delegates to the inner message.
func (*LazyMessage) SetUnknown ¶
func (lm *LazyMessage) SetUnknown(b []byte)
SetUnknown replaces the raw bytes of unknown fields. It triggers resolution, marks the message as dirty, and delegates to the inner message.
func (*LazyMessage) Size ¶
func (lm *LazyMessage) Size() int
Size returns the serialized size of the message. If the message has not been accessed or modified, it returns the length of the raw bytes without parsing.
func (*LazyMessage) Unmarshal ¶
func (lm *LazyMessage) Unmarshal(b []byte) error
Unmarshal replaces the message state with the decoded contents of b. It stores a defensive copy of b, resets the inner message and sync.Once, and clears the dirty flag so subsequent reads trigger a fresh resolve.
func (*LazyMessage) Validate ¶
func (lm *LazyMessage) Validate() error
Validate checks schema constraints on the message. It triggers resolution before delegating to the inner message's Validate method.
func (*LazyMessage) WhichOneof ¶
func (lm *LazyMessage) WhichOneof(od descriptor.OneofDescriptorAccessor) descriptor.FieldDescriptorAccessor
WhichOneof returns the field descriptor of the currently set field in the given oneof. It triggers resolution before delegating to the inner message.
type List ¶
type List struct {
// contains filtered or unexported fields
}
List is a repeated field container implementing protoreflect.List. It stores elements as protoreflect.Value entries in a slice and tracks the element kind for NewElement construction. Lists created via NewList are valid; read-only sentinel lists returned for unset repeated fields are invalid.
func NewList ¶
NewList creates a new valid List for elements of the given scalar kind. The underlying slice is nil until the first Append.
func (*List) Append ¶
func (l *List) Append(v protoreflect.Value)
Append appends the given value to the end of the list.
func (*List) Get ¶
func (l *List) Get(i int) protoreflect.Value
Get returns the element at the given index. It panics if the index is out of range.
func (*List) IsValid ¶
IsValid reports whether the list is usable. Lists created via NewList return true; read-only sentinels for unset repeated fields return false.
func (*List) NewElement ¶
func (l *List) NewElement() protoreflect.Value
NewElement returns the zero-value for the list's element type. For message elements it returns a new dynamic message wrapping the stored message descriptor. For scalar elements it returns the proto3 zero-value.
type Map ¶
type Map struct {
// contains filtered or unexported fields
}
Map implements protoreflect.Map for dynamically-typed protobuf map fields. It stores entries in a Go map keyed by the MapKey's Interface() value for O(1) lookup, and tracks the key and value scalar kinds for NewValue construction and MapKey reconstruction during Range iteration.
func NewMap ¶
NewMap creates a new empty Map with the given key and value scalar kinds. The returned Map is valid and ready for use.
func (*Map) Clear ¶
func (m *Map) Clear(k protoreflect.MapKey)
Clear removes the entry for the given key from the map. If the key is not present, Clear is a no-op.
func (*Map) Get ¶
func (m *Map) Get(k protoreflect.MapKey) protoreflect.Value
Get returns the value associated with the given key. If the key is not present, it returns an invalid Value (where IsValid returns false).
func (*Map) Has ¶
func (m *Map) Has(k protoreflect.MapKey) bool
Has reports whether the map contains an entry for the given key.
func (*Map) IsValid ¶
IsValid reports whether this Map is usable. Maps created via NewMap or newMapWithMessageDesc return true. The read-only sentinel created via newInvalidMap returns false.
func (*Map) NewValue ¶
func (m *Map) NewValue() protoreflect.Value
NewValue returns the zero/default Value for the map's value type. For message-typed values it constructs a new dynamic message via the newMessageFunc factory. For scalar values it returns the proto3 zero default.
func (*Map) Range ¶
func (m *Map) Range(f func(protoreflect.MapKey, protoreflect.Value) bool)
Range iterates over all entries in the map, calling f for each key-value pair. The MapKey is reconstructed from the stored Go key value using the map's key kind. Iteration stops early if f returns false. Iteration order is undefined, matching Go map semantics.
func (*Map) Set ¶
func (m *Map) Set(k protoreflect.MapKey, v protoreflect.Value)
Set inserts or updates the entry for the given key with the provided value. The entries map is lazily initialized on first Set.
type MarshalOptions ¶
type MarshalOptions struct {
// Deterministic causes map fields to be serialized with keys in sorted
// order: bool false before true, integers numerically, strings
// lexicographically. When false (the default), map iteration order is
// undefined.
Deterministic bool
// AllowPartial allows marshaling a message that has missing required fields.
// When false (the default), Marshal returns ErrRequiredNotSet if any proto2
// required field is not set. When true, required field validation is skipped.
AllowPartial bool
}
MarshalOptions controls how a dynamic Message is encoded to wire format.
func (MarshalOptions) Marshal ¶
func (opts MarshalOptions) Marshal(msg *Message) ([]byte, error)
Marshal encodes msg to wire format using the configured options.
func (MarshalOptions) MarshalAppend ¶
func (opts MarshalOptions) MarshalAppend(b []byte, msg *Message) ([]byte, error)
MarshalAppend appends the wire-format encoding of msg to b and returns the extended slice. When AllowPartial is false, required fields are validated before encoding. When Deterministic is true, map keys are sorted before encoding.
type Message ¶
type Message struct {
// contains filtered or unexported fields
}
Message is a dynamic protobuf message that implements both protoreflect.Message and proto.Message. It stores field values in a map keyed by field number, with pre-built lookup tables for O(1) field and oneof dispatch.
fieldalignment: fields grouped by lifecycle (descriptor, lookup, state)
func New ¶
func New(desc descriptor.MessageDescriptorAccessor) *Message
New creates a new dynamic Message from a MessageDescriptorAccessor. It panics if desc is nil.
func (*Message) Clear ¶
func (m *Message) Clear(fd descriptor.FieldDescriptorAccessor)
Clear removes a field from the values map, returning it to its default state.
func (*Message) Descriptor ¶
func (m *Message) Descriptor() descriptor.MessageDescriptorAccessor
Descriptor returns the message descriptor passed to New.
func (*Message) Get ¶
func (m *Message) Get(fd descriptor.FieldDescriptorAccessor) protoreflect.Value
Get reads a field value by its field descriptor. For unset fields it returns the appropriate default: zero-values for scalars, invalid sentinels for messages, lists, and maps.
func (*Message) GetUnknown ¶
GetUnknown returns the raw bytes of unknown fields preserved during decoding.
func (*Message) Has ¶
func (m *Message) Has(fd descriptor.FieldDescriptorAccessor) bool
Has reports whether a field is populated.
func (*Message) Interface ¶
func (m *Message) Interface() protoreflect.ProtoMessage
Interface returns m as a ProtoMessage. The Message is its own ProtoMessage.
func (*Message) IsValid ¶
IsValid reports whether the message was constructed via New. Messages created as invalid sentinels for unset message fields return false.
func (*Message) MarshalAppend ¶
MarshalAppend appends the wire-format encoding of the message to b and returns the extended slice.
func (*Message) Merge ¶
Merge merges the fields of src into this message. Singular scalar fields are overwritten, repeated fields are appended, map fields merge by key, and nested messages are recursively merged.
func (*Message) New ¶
func (m *Message) New() protoreflect.Message
New returns a fresh empty dynamic message with the same descriptor.
func (*Message) ProtoReflect ¶
func (m *Message) ProtoReflect() protoreflect.Message
ProtoReflect returns m as a protoreflect.Message, bridging the proto.Message and protoreflect.Message interface hierarchies.
func (*Message) Range ¶
func (m *Message) Range(f func(descriptor.FieldDescriptorAccessor, protoreflect.Value) bool)
Range iterates over every populated field in ascending field number order.
func (*Message) Reset ¶
func (m *Message) Reset()
Reset clears all field values, oneof tracking, extension tracking, and unknown fields, returning the message to its zero state.
func (*Message) Set ¶
func (m *Message) Set(fd descriptor.FieldDescriptorAccessor, v protoreflect.Value)
Set writes a field value by its field descriptor. It validates that the value kind matches the expected kind for non-list/non-map fields and handles oneof mutual exclusivity.
func (*Message) SetUnknown ¶
SetUnknown replaces the raw bytes of unknown fields. The input is defensively copied to prevent aliasing.
func (*Message) Unmarshal ¶
Unmarshal decodes wire-format bytes into the message. Unknown fields are accumulated and stored after decoding. Group wire types are handled by validating the group body via SkipField before storing as unknown bytes. Unmarshal merges into existing state and does not reset the message first.
func (*Message) Validate ¶
Validate checks schema constraints on the message. It verifies that all required fields are present and that closed enum fields contain valid values.
func (*Message) WhichOneof ¶
func (m *Message) WhichOneof(od descriptor.OneofDescriptorAccessor) descriptor.FieldDescriptorAccessor
WhichOneof returns the field descriptor of the currently set field in the given oneof, or nil if no field in the oneof is set.
type UnmarshalOptions ¶
type UnmarshalOptions struct {
// Resolver is an optional extension resolver for resolving unrecognized
// field numbers against registered extensions.
Resolver registry.ExtensionResolver
// AllowPartial allows unmarshaling a message that has missing required
// fields. When false (the default), Unmarshal returns ErrRequiredNotSet
// if any proto2 required field is not present after decoding. When true,
// required field validation is skipped.
AllowPartial bool
// MaxAllocationSize sets the maximum number of bytes that a single
// length-delimited field may claim during decoding. A value of 0 means
// use the default limit of 2 GiB (wire.MaxMessageSize). Payloads where a
// length prefix exceeds this limit or the remaining input buffer are
// rejected with decode.ErrMessageTooLarge.
MaxAllocationSize int
// RecursionLimit sets the maximum nesting depth allowed during unmarshal.
// A value of 0 means use the default limit of 10,000. Payloads where
// nested message fields exceed this depth are rejected with
// ErrRecursionLimitExceeded.
RecursionLimit int
}
UnmarshalOptions controls how wire bytes are decoded into a dynamic Message. When a Resolver is provided, unrecognized field numbers are resolved against the extension registry before falling back to unknown field storage.
fieldalignment: fields ordered for semantic clarity, not padding
func (UnmarshalOptions) Unmarshal ¶
func (opts UnmarshalOptions) Unmarshal(b []byte, msg *Message) error
Unmarshal decodes wire-format bytes into msg. If opts.Resolver is nil, it delegates to the message's built-in unmarshal path. Otherwise it runs the same decode loop but resolves unknown field numbers through the extension resolver before treating them as unknown fields. When AllowPartial is false, required fields are validated after decoding.