Skip to main content

Encoders and Decoders

KiviGo supports pluggable encoders that handle the serialization and deserialization of your data. This allows you to choose the best encoding format for your use case while maintaining the same API.

Creating Custom Encoders​

You can create custom encoders by implementing the Encoder interface:

package main

import (
"encoding/xml"

"github.com/kivigo/kivigo/pkg/models"
)

// Custom XML encoder
type XMLEncoder struct{}

func (e XMLEncoder) Encode(v interface{}) ([]byte, error) {
return xml.Marshal(v)
}

func (e XMLEncoder) Decode(data []byte, v interface{}) error {
return xml.Unmarshal(data, v)
}

// Ensure it implements the interface
var _ models.Encoder = (*XMLEncoder)(nil)

func main() {
// Use custom XML encoder
xmlClient, err := client.New(kvStore, client.Option{
Encoder: XMLEncoder{},
})

// Use the same API with XML encoding
err = xmlClient.Set(ctx, "data", MyStruct{...})
}

Advanced Encoder Features​

Compression Encoder​

Wrap existing encoders with compression:

import (
"bytes"
"compress/gzip"
"io"
)

type CompressedEncoder struct {
inner models.Encoder
}

func (e CompressedEncoder) Encode(v interface{}) ([]byte, error) {
// First encode with inner encoder
data, err := e.inner.Encode(v)
if err != nil {
return nil, err
}

// Then compress
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)

_, err = gz.Write(data)
if err != nil {
return nil, err
}

err = gz.Close()
if err != nil {
return nil, err
}

return buf.Bytes(), nil
}

func (e CompressedEncoder) Decode(data []byte, v interface{}) error {
// First decompress
gz, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return err
}
defer gz.Close()

decompressed, err := io.ReadAll(gz)
if err != nil {
return err
}

// Then decode with inner encoder
return e.inner.Decode(decompressed, v)
}

// Usage
compressedJSON := CompressedEncoder{inner: encoder.JSON}
client, err := client.New(kvStore, client.Option{
Encoder: compressedJSON,
})

Performance Considerations​

Encoding Benchmarks​

Different encoders have different performance characteristics:

func BenchmarkEncoders(b *testing.B) {
data := LargeStruct{...}

encoders := map[string]models.Encoder{
"JSON": encoder.JSON,
"YAML": encoder.YAML,
"XML": XMLEncoder{},
}

for name, enc := range encoders {
b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
encoded, _ := enc.Encode(data)
var decoded LargeStruct
enc.Decode(encoded, &decoded)
}
})
}
}

Choosing the Right Encoder​

EncoderSpeedSizeHuman ReadableSchema Evolution
JSONFastMedium✅Good
YAMLSlowerLarger✅Good
XMLSlowerLargest✅Excellent
BinaryFastestSmallest❌Limited
CompressedMediumSmallest❌Same as inner

Error Handling​

Handle encoding/decoding errors appropriately:

import "github.com/kivigo/kivigo/pkg/errs"

func safeEncodeDecode() {
var result MyStruct
err := client.Get(ctx, "key", &result)

if err != nil {
switch {
case errors.Is(err, errs.ErrNotFound):
log.Println("Key not found")
case errors.Is(err, errs.ErrInvalidData):
log.Println("Data could not be decoded")
default:
log.Printf("Other error: %v", err)
}
}
}

Best Practices​

1. Consistent Encoding​

Use the same encoder for a given key across your application:

// Bad: Different encoders for same logical data
jsonClient.Set(ctx, "user:1", user)
yamlClient.Get(ctx, "user:1", &user) // Will fail!

// Good: Same encoder throughout
jsonClient.Set(ctx, "user:1", user)
jsonClient.Get(ctx, "user:1", &user) // Works!

2. Schema Evolution​

Design your structs for backward compatibility:

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`

// New fields should be optional with omitempty
Phone string `json:"phone,omitempty"`
Age int `json:"age,omitempty"`
}

3. Performance Testing​

Always benchmark your encoder choice with real data:

func TestEncoderPerformance(t *testing.T) {
realData := getRealWorldData()

start := time.Now()
encoded, err := encoder.JSON.Encode(realData)
encodeTime := time.Since(start)

start = time.Now()
var decoded MyStruct
err = encoder.JSON.Decode(encoded, &decoded)
decodeTime := time.Since(start)

t.Logf("JSON: Encode=%v, Decode=%v, Size=%d bytes",
encodeTime, decodeTime, len(encoded))
}

4. Validation​

Validate data after decoding:

type ValidatedStruct struct {
Email string `json:"email"`
Age int `json:"age"`
}

func (v *ValidatedStruct) Validate() error {
if v.Email == "" {
return errors.New("email is required")
}
if v.Age < 0 || v.Age > 150 {
return errors.New("invalid age")
}
return nil
}

// Usage
var data ValidatedStruct
err := client.Get(ctx, "key", &data)
if err != nil {
return err
}

if err := data.Validate(); err != nil {
return fmt.Errorf("invalid data: %w", err)
}

Encoders provide flexibility in how your data is stored and transmitted. Choose the right encoder for your use case and always consider performance, size, and compatibility requirements.