main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
// user.proto (reference)
/*
syntax = "proto3";

package user;
option go_package = "example.com/project/userpb";

// UserType defines user account types
enum UserType {
  USER_TYPE_UNSPECIFIED = 0;
  USER_TYPE_STANDARD = 1;
  USER_TYPE_PREMIUM = 2;
  USER_TYPE_ADMIN = 3;
}

// User represents a user profile
message User {
  string id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  UserType type = 5;

  message Address {
    string street = 1;
    string city = 2;
    string state = 3;
    string zip_code = 4;
    string country = 5;
  }

  Address address = 6;
  repeated string hobbies = 7;
}
*/

// If you want generated Go types (recommended for production), install protoc + plugins and run:
//   protoc --go_out=. --go_opt=paths=source_relative user.proto
//
// This runnable snippet avoids `protoc` by building protobuf descriptors at runtime (dynamicpb),
// then demonstrates binary + JSON (protojson) serialization/deserialization with proper errors.

package main

import (
	"fmt"
	"log"
	"os"

	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protodesc"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/types/descriptorpb"
	"google.golang.org/protobuf/types/dynamicpb"
)

func buildSchema() (protoreflect.MessageDescriptor, protoreflect.EnumDescriptor, error) {
	userTypeEnum := &descriptorpb.EnumDescriptorProto{
		Name: proto.String("UserType"),
		Value: []*descriptorpb.EnumValueDescriptorProto{
			{Name: proto.String("USER_TYPE_UNSPECIFIED"), Number: proto.Int32(0)},
			{Name: proto.String("USER_TYPE_STANDARD"), Number: proto.Int32(1)},
			{Name: proto.String("USER_TYPE_PREMIUM"), Number: proto.Int32(2)},
			{Name: proto.String("USER_TYPE_ADMIN"), Number: proto.Int32(3)},
		},
	}

	addressMsg := &descriptorpb.DescriptorProto{
		Name: proto.String("Address"),
		Field: []*descriptorpb.FieldDescriptorProto{
			{Name: proto.String("street"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
			{Name: proto.String("city"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
			{Name: proto.String("state"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
			{Name: proto.String("zip_code"), Number: proto.Int32(4), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
			{Name: proto.String("country"), Number: proto.Int32(5), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
		},
	}

	userMsg := &descriptorpb.DescriptorProto{
		Name: proto.String("User"),
		Field: []*descriptorpb.FieldDescriptorProto{
			{Name: proto.String("id"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
			{Name: proto.String("name"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
			{Name: proto.String("email"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
			{Name: proto.String("age"), Number: proto.Int32(4), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
			{Name: proto.String("type"), Number: proto.Int32(5), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_ENUM.Enum(), TypeName: proto.String(".user.UserType")},
			{Name: proto.String("address"), Number: proto.Int32(6), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: proto.String(".user.User.Address")},
			{Name: proto.String("hobbies"), Number: proto.Int32(7), Label: descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
		},
		NestedType: []*descriptorpb.DescriptorProto{addressMsg},
	}

	file := &descriptorpb.FileDescriptorProto{
		Syntax:  proto.String("proto3"),
		Name:    proto.String("user.proto"),
		Package: proto.String("user"),
		EnumType: []*descriptorpb.EnumDescriptorProto{
			userTypeEnum,
		},
		MessageType: []*descriptorpb.DescriptorProto{
			userMsg,
		},
	}

	files, err := protodesc.NewFiles(&descriptorpb.FileDescriptorSet{File: []*descriptorpb.FileDescriptorProto{file}})
	if err != nil {
		return nil, nil, fmt.Errorf("build descriptor set: %w", err)
	}
	fd, err := files.FindFileByPath("user.proto")
	if err != nil {
		return nil, nil, fmt.Errorf("find file: %w", err)
	}

	msg := fd.Messages().ByName("User")
	if msg == nil {
		return nil, nil, fmt.Errorf("message User not found")
	}
	en := fd.Enums().ByName("UserType")
	if en == nil {
		return nil, nil, fmt.Errorf("enum UserType not found")
	}
	return msg, en, nil
}

func newSampleUser(userDesc protoreflect.MessageDescriptor, userTypeEnum protoreflect.EnumDescriptor) (*dynamicpb.Message, error) {
	user := dynamicpb.NewMessage(userDesc)
	fields := userDesc.Fields()

	user.Set(fields.ByName("id"), protoreflect.ValueOfString("12345"))
	user.Set(fields.ByName("name"), protoreflect.ValueOfString("John Doe"))
	user.Set(fields.ByName("email"), protoreflect.ValueOfString("[email protected]"))
	user.Set(fields.ByName("age"), protoreflect.ValueOfInt32(30))

	premium := userTypeEnum.Values().ByName("USER_TYPE_PREMIUM")
	if premium == nil {
		return nil, fmt.Errorf("enum value USER_TYPE_PREMIUM not found")
	}
	user.Set(fields.ByName("type"), protoreflect.ValueOfEnum(premium.Number()))

	addrDesc := userDesc.Messages().ByName("Address")
	if addrDesc == nil {
		return nil, fmt.Errorf("nested message Address not found")
	}
	addr := dynamicpb.NewMessage(addrDesc)
	addrFields := addrDesc.Fields()
	addr.Set(addrFields.ByName("street"), protoreflect.ValueOfString("123 Main St"))
	addr.Set(addrFields.ByName("city"), protoreflect.ValueOfString("New York"))
	addr.Set(addrFields.ByName("state"), protoreflect.ValueOfString("NY"))
	addr.Set(addrFields.ByName("zip_code"), protoreflect.ValueOfString("10001"))
	addr.Set(addrFields.ByName("country"), protoreflect.ValueOfString("USA"))

	user.Set(fields.ByName("address"), protoreflect.ValueOfMessage(addr))

	hobbiesField := fields.ByName("hobbies")
	list := user.Mutable(hobbiesField).List()
	list.Append(protoreflect.ValueOfString("reading"))
	list.Append(protoreflect.ValueOfString("hiking"))
	list.Append(protoreflect.ValueOfString("coding"))

	return user, nil
}

func SerializeToBinary(msg proto.Message) ([]byte, error) {
	if msg == nil {
		return nil, fmt.Errorf("message cannot be nil")
	}
	b, err := proto.Marshal(msg)
	if err != nil {
		return nil, fmt.Errorf("marshal (binary): %w", err)
	}
	return b, nil
}

func DeserializeFromBinary(desc protoreflect.MessageDescriptor, data []byte) (*dynamicpb.Message, error) {
	if len(data) == 0 {
		return nil, fmt.Errorf("binary data cannot be empty")
	}
	msg := dynamicpb.NewMessage(desc)
	if err := proto.Unmarshal(data, msg); err != nil {
		return nil, fmt.Errorf("unmarshal (binary): %w", err)
	}
	return msg, nil
}

func SerializeToJSON(msg proto.Message) (string, error) {
	if msg == nil {
		return "", fmt.Errorf("message cannot be nil")
	}
	b, err := protojson.MarshalOptions{
		Indent:        "  ",
		UseProtoNames: true,
	}.Marshal(msg)
	if err != nil {
		return "", fmt.Errorf("marshal (json): %w", err)
	}
	return string(b), nil
}

func DeserializeFromJSON(desc protoreflect.MessageDescriptor, jsonStr string) (*dynamicpb.Message, error) {
	if jsonStr == "" {
		return nil, fmt.Errorf("JSON string cannot be empty")
	}
	msg := dynamicpb.NewMessage(desc)
	if err := protojson.Unmarshal([]byte(jsonStr), msg); err != nil {
		return nil, fmt.Errorf("unmarshal (json): %w", err)
	}
	return msg, nil
}

func main() {
	userDesc, userTypeEnum, err := buildSchema()
	if err != nil {
		log.Fatal(err)
	}

	user, err := newSampleUser(userDesc, userTypeEnum)
	if err != nil {
		log.Fatal(err)
	}

	binaryData, err := SerializeToBinary(user)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Binary data length: %d bytes\n", len(binaryData))

	decoded, err := DeserializeFromBinary(userDesc, binaryData)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Decoded name: %s\n", decoded.ProtoReflect().Get(userDesc.Fields().ByName("name")).String())

	jsonStr, err := SerializeToJSON(user)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("\nJSON representation:\n" + jsonStr)

	decodedFromJSON, err := DeserializeFromJSON(userDesc, jsonStr)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Decoded email from JSON: %s\n", decodedFromJSON.ProtoReflect().Get(userDesc.Fields().ByName("email")).String())

	if err := os.WriteFile("user_data.bin", binaryData, 0o644); err != nil {
		log.Fatal(err)
	}
	fmt.Println("\nBinary data saved to user_data.bin")
}

How It Works

Defines proto3 schemas with enums and nested messages and uses dynamicpb to serialize and deserialize without generated code.

Loads compiled descriptors, creates dynamic messages, sets fields, marshals to binary and JSON, then unmarshals back to Go structs while handling errors.

Key Concepts

  • 1Uses descriptorpb and dynamicpb to work with protos at runtime.
  • 2Serializes to both binary wire format and the JSON mapping.
  • 3Validates required fields and enum values through descriptor metadata.

When to Use This Pattern

  • Building tooling that manipulates arbitrary proto schemas.
  • Interoperating with protos without generating bindings.
  • Testing or debugging serialized payloads quickly.

Best Practices

  • Keep descriptors in sync with the proto definitions.
  • Validate field presence and enum values before sending.
  • Handle unknown fields gracefully when consuming external data.
Go Version1.19
Difficultyintermediate
Production ReadyYes
Lines of Code251