zoobzio December 10, 2025 Edit this page

Codecs

Codecs handle message serialization and deserialization. Herald uses JSON by default, but supports custom codecs for alternative formats.

Codec Interface

type Codec interface {
    Marshal(v any) ([]byte, error)
    Unmarshal(data []byte, v any) error
    ContentType() string
}

Default JSON Codec

Herald provides a built-in JSON codec:

type JSONCodec struct{}

func (JSONCodec) Marshal(v any) ([]byte, error) {
    return json.Marshal(v)
}

func (JSONCodec) Unmarshal(data []byte, v any) error {
    return json.Unmarshal(data, v)
}

func (JSONCodec) ContentType() string {
    return "application/json"
}

Custom Codec Example

Protocol Buffers

type ProtobufCodec struct{}

func (ProtobufCodec) Marshal(v any) ([]byte, error) {
    msg, ok := v.(proto.Message)
    if !ok {
        return nil, fmt.Errorf("value must implement proto.Message")
    }
    return proto.Marshal(msg)
}

func (ProtobufCodec) Unmarshal(data []byte, v any) error {
    msg, ok := v.(proto.Message)
    if !ok {
        return fmt.Errorf("value must implement proto.Message")
    }
    return proto.Unmarshal(data, msg)
}

func (ProtobufCodec) ContentType() string {
    return "application/protobuf"
}

MessagePack

import "github.com/vmihailenco/msgpack/v5"

type MsgpackCodec struct{}

func (MsgpackCodec) Marshal(v any) ([]byte, error) {
    return msgpack.Marshal(v)
}

func (MsgpackCodec) Unmarshal(data []byte, v any) error {
    return msgpack.Unmarshal(data, v)
}

func (MsgpackCodec) ContentType() string {
    return "application/msgpack"
}

Using Custom Codecs

With Publisher

pub := herald.NewPublisher(provider, signal, key, nil,
    herald.WithPublisherCodec[Order](ProtobufCodec{}))

With Subscriber

sub := herald.NewSubscriber(provider, signal, key, nil,
    herald.WithSubscriberCodec[Order](ProtobufCodec{}))

Content-Type Header

The codec's ContentType() is automatically added to message metadata:

// Publishing with JSON codec
// Metadata includes: {"Content-Type": "application/json"}

// Publishing with custom codec
// Metadata includes: {"Content-Type": "application/protobuf"}

Existing Content-Type in metadata is not overwritten:

// Middleware that sets Content-Type before the terminal
opts := []herald.Option[Order]{
    herald.WithMiddleware(
        herald.UseTransform[Order]("set-ct", func(_ context.Context, env *herald.Envelope[Order]) *herald.Envelope[Order] {
            env.Metadata["Content-Type"] = "application/x-custom"
            return env
        }),
    ),
}
// This Content-Type is preserved by the publish terminal

Codec Matching

Publishers and subscribers must use compatible codecs:

// Service A: Publishes with Protobuf
pub := herald.NewPublisher(provider, signal, key, nil,
    herald.WithPublisherCodec[Order](ProtobufCodec{}))

// Service B: Must subscribe with Protobuf
sub := herald.NewSubscriber(provider, signal, key, nil,
    herald.WithSubscriberCodec[Order](ProtobufCodec{}))

Mismatched codecs result in deserialization errors:

capitan.Hook(herald.ErrorSignal, func(ctx context.Context, e *capitan.Event) {
    err, _ := herald.ErrorKey.From(e)
    if err.Operation == "unmarshal" {
        // Likely codec mismatch
        log.Printf("Deserialization failed: %v", err.Err)
        log.Printf("Raw payload: %s", err.Raw)
    }
})

Performance Considerations

CodecSizeSpeedSchema
JSONLargerModerateNo
ProtobufSmallestFastRequired
MessagePackSmallFastNo

Choose based on your requirements:

  • JSON: Human-readable, universal compatibility
  • Protobuf: Maximum performance, schema evolution
  • MessagePack: Good balance, no schema needed

Testing with Codecs

func TestCustomCodec(t *testing.T) {
    codec := MsgpackCodec{}

    original := Order{ID: "123", Total: 99.99}

    // Marshal
    data, err := codec.Marshal(original)
    if err != nil {
        t.Fatalf("marshal failed: %v", err)
    }

    // Unmarshal
    var decoded Order
    err = codec.Unmarshal(data, &decoded)
    if err != nil {
        t.Fatalf("unmarshal failed: %v", err)
    }

    if decoded != original {
        t.Errorf("round-trip failed: got %+v, want %+v", decoded, original)
    }
}