Skip to main content
Version: 1.4.0

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.

Available Encoders

KiviGo comes with built-in support for several encoding formats:

JSON Encoder (Default)

import "github.com/azrod/kivigo/pkg/encoder"

client, err := client.New(kvStore, client.Option{
Encoder: encoder.JSON, // This is the default
})

YAML Encoder

import "github.com/azrod/kivigo/pkg/encoder"

client, err := client.New(kvStore, client.Option{
Encoder: encoder.YAML,
})

Using Different Encoders

JSON Example

package main

import (
"context"
"fmt"
"log"

"github.com/azrod/kivigo"
"github.com/azrod/kivigo/pkg/encoder"
"github.com/azrod/kivigo/backend/badger"
)

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

func main() {
kvStore, _ := badger.New(badger.DefaultOptions("./data"))
defer kvStore.Close()

// JSON encoder (default)
jsonClient, err := client.New(kvStore, client.Option{
Encoder: encoder.JSON,
})
if err != nil {
log.Fatal(err)
}

ctx := context.Background()
user := User{ID: 1, Name: "John", Email: "john@example.com"}

// Store with JSON encoding
err = jsonClient.Set(ctx, "user:1", user)
if err != nil {
log.Fatal(err)
}

// Retrieve with JSON decoding
var retrievedUser User
err = jsonClient.Get(ctx, "user:1", &retrievedUser)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Retrieved user: %+v\n", retrievedUser)
}

YAML Example

// YAML encoder
yamlClient, err := client.New(kvStore, client.Option{
Encoder: encoder.YAML,
})

// Same API, different encoding
err = yamlClient.Set(ctx, "config", ConfigStruct{...})

Creating Custom Encoders

You can create custom encoders by implementing the Encoder interface:

package main

import (
"encoding/xml"

"github.com/azrod/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,
})

Encryption Encoder

Add encryption to your data:

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
)

type EncryptedEncoder struct {
inner models.Encoder
gcm cipher.AEAD
}

func NewEncryptedEncoder(inner models.Encoder, key []byte) (*EncryptedEncoder, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

return &EncryptedEncoder{
inner: inner,
gcm: gcm,
}, nil
}

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

// Create random nonce
nonce := make([]byte, e.gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}

// Encrypt data
ciphertext := e.gcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}

func (e *EncryptedEncoder) Decode(data []byte, v interface{}) error {
// Extract nonce and ciphertext
nonceSize := e.gcm.NonceSize()
if len(data) < nonceSize {
return fmt.Errorf("ciphertext too short")
}

nonce, ciphertext := data[:nonceSize], data[nonceSize:]

// Decrypt
plaintext, err := e.gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return err
}

// Decode with inner encoder
return e.inner.Decode(plaintext, v)
}

// Usage
key := []byte("32-byte-long-key-for-aes-256-gcm!") // 32 bytes for AES-256
encryptedJSON, err := NewEncryptedEncoder(encoder.JSON, key)
if err != nil {
log.Fatal(err)
}

client, err := client.New(kvStore, client.Option{
Encoder: encryptedJSON,
})

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
JSONFastMediumGood
YAMLSlowerLargerGood
XMLSlowerLargestExcellent
BinaryFastestSmallestLimited
CompressedMediumSmallestSame as inner

Error Handling

Handle encoding/decoding errors appropriately:

import "github.com/azrod/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.