Recipes

A typed JSON repository

Wrap the byte API in application validation, a versioned JSON envelope, and typed reads.

Run the complete example

From the repository root:

go run ./examples/jsonstore
1024: Ada (version 1)
missing user: handled with errors.Is

The example creates a temporary database, closes it, and removes only its own temporary directory. The complete source below is embedded from the runnable repository example during the site build.

Define an application envelope

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

type userEnvelope struct {
    Schema int  `json:"schema"`
    User   User `json:"user"`
}

The envelope schema is an application encoding version. A record’s commit Version is a separate concept and must not be used to infer JSON shape.

Validate before encoding

func (s UserStore) Put(ctx context.Context, user User) error {
    if user.ID == "" || user.Name == "" {
        return errors.New("user id and name are required")
    }
    data, err := json.Marshal(userEnvelope{Schema: 1, User: user})
    if err != nil {
        return err
    }
    return s.DB.Put(ctx, "users", []byte(user.ID), data)
}

UserStore is application code holding a *zeedb.DB; it is not a built-in SDK type. The complete example contains the imports and type declaration.

The engine’s key/value/batch limits still apply to the encoded payload. Application validation cannot substitute for checking the returned DB error.

Decode deliberately

The example uses GetRecord, then checks:

  1. The engine read succeeded, with ErrNotFound handled separately.
  2. The value decodes as JSON.
  3. The envelope schema is supported.
  4. The ID inside the payload matches the requested key.
  5. The stored name satisfies the example’s required-field rule.

Do not silently accept a decoding error as an empty user. A good checksum does not imply valid application JSON.

Preserve error identity

Wrap application decoding errors with %w when callers need to inspect their type. Preserve DB sentinels so callers can use errors.Is(err, zeedb.ErrNotFound).

_, _, err := store.Get(ctx, "missing")
if errors.Is(err, zeedb.ErrNotFound) {
    // Expected absence; choose your application's response.
} else if err != nil {
    return err
}

Add updates carefully

This repository demonstrates replacement writes, not compare-and-swap. Reading a user/version and writing a changed user does not protect against another writer. Coordinate the entire read/validate/write path at the application layer when preserving an invariant matters.

For logical indexes, commit primary data and index entries together; see manual indexes.

Complete source

This is the canonical examples/jsonstore/main.go, including imports, lifecycle, application validation, and missing-record handling. The page loads it at build time so the complete listing stays aligned with the executable example.

// Run from the repository root: go run ./examples/jsonstore
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"

	zeedb "github.com/Berlindraft/ZeeDB"
)

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}
type userEnvelope struct {
	Schema int  `json:"schema"`
	User   User `json:"user"`
}
type UserStore struct{ DB *zeedb.DB }

func (s UserStore) Put(ctx context.Context, user User) error {
	if user.ID == "" || user.Name == "" {
		return errors.New("user id and name are required")
	}
	data, err := json.Marshal(userEnvelope{Schema: 1, User: user})
	if err != nil {
		return err
	}
	return s.DB.Put(ctx, "users", []byte(user.ID), data)
}
func (s UserStore) Get(ctx context.Context, id string) (User, uint64, error) {
	r, err := s.DB.GetRecord(ctx, "users", []byte(id))
	if err != nil {
		return User{}, 0, err
	}
	var envelope userEnvelope
	if err := json.Unmarshal(r.Value, &envelope); err != nil {
		return User{}, 0, fmt.Errorf("decode user %q: %w", id, err)
	}
	if envelope.Schema != 1 {
		return User{}, 0, fmt.Errorf("unsupported user schema %d", envelope.Schema)
	}
	if envelope.User.ID != id {
		return User{}, 0, errors.New("user key/payload id mismatch")
	}
	if envelope.User.Name == "" {
		return User{}, 0, errors.New("stored user name is required")
	}
	return envelope.User, r.Version, nil
}

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
func run() (err error) {
	dir, err := os.MkdirTemp("", "zeedb-json-")
	if err != nil {
		return err
	}
	defer func() { err = errors.Join(err, os.RemoveAll(dir)) }()
	db, err := zeedb.Open(dir, zeedb.Options{})
	if err != nil {
		return err
	}
	defer func() { err = errors.Join(err, db.Close()) }()
	store := UserStore{DB: db}
	ctx := context.Background()
	if err := store.Put(ctx, User{ID: "1024", Name: "Ada"}); err != nil {
		return err
	}
	user, version, err := store.Get(ctx, "1024")
	if err != nil {
		return err
	}
	fmt.Printf("%s: %s (version %d)\n", user.ID, user.Name, version)
	_, _, err = store.Get(ctx, "missing")
	if !errors.Is(err, zeedb.ErrNotFound) {
		return fmt.Errorf("expected ErrNotFound, got %v", err)
	}
	fmt.Println("missing user: handled with errors.Is")
	return nil
}
Something to improve?Edit this page on GitHub