dynamicpb

package
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 30, 2026 License: MIT Imports: 15 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
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.

View Source
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

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

Descriptor returns the message descriptor without triggering resolution.

func (*LazyMessage) Get

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

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

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

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

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

func NewList(elementKind scalar.Kind) *List

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

func (l *List) IsValid() bool

IsValid reports whether the list is usable. Lists created via NewList return true; read-only sentinels for unset repeated fields return false.

func (*List) Len

func (l *List) Len() int

Len returns the number of elements in the list.

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.

func (*List) Set

func (l *List) Set(i int, v protoreflect.Value)

Set replaces the element at the given index with the provided value. It panics if the index is out of range.

func (*List) Truncate

func (l *List) Truncate(n int)

Truncate reduces the list length to n elements. It panics if n is negative or greater than Len.

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

func NewMap(keyKind, valueKind scalar.Kind) *Map

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

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

func (m *Map) IsValid() bool

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) Len

func (m *Map) Len() int

Len returns the number of entries in the map.

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

New creates a new dynamic Message from a MessageDescriptorAccessor. It panics if desc is nil.

func (*Message) Clear

Clear removes a field from the values map, returning it to its default state.

func (*Message) Clone

func (m *Message) Clone() proto.Message

Clone returns a deep copy of this message as a proto.Message.

func (*Message) Descriptor

Descriptor returns the message descriptor passed to New.

func (*Message) Get

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

func (m *Message) GetUnknown() []byte

GetUnknown returns the raw bytes of unknown fields preserved during decoding.

func (*Message) Has

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

func (m *Message) IsValid() bool

IsValid reports whether the message was constructed via New. Messages created as invalid sentinels for unset message fields return false.

func (*Message) Marshal

func (m *Message) Marshal() ([]byte, error)

Marshal returns the wire-format encoding of the message.

func (*Message) MarshalAppend

func (m *Message) MarshalAppend(b []byte) ([]byte, error)

MarshalAppend appends the wire-format encoding of the message to b and returns the extended slice.

func (*Message) Merge

func (m *Message) Merge(src proto.Message) error

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

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

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

func (m *Message) SetUnknown(b []byte)

SetUnknown replaces the raw bytes of unknown fields. The input is defensively copied to prevent aliasing.

func (*Message) Size

func (m *Message) Size() int

Size returns the exact number of bytes that Marshal would produce.

func (*Message) Unmarshal

func (m *Message) Unmarshal(b []byte) error

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

func (m *Message) Validate() error

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

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL