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
| Codec | Size | Speed | Schema |
|---|---|---|---|
| JSON | Larger | Moderate | No |
| Protobuf | Smallest | Fast | Required |
| MessagePack | Small | Fast | No |
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)
}
}