Zema Logo
Plugins

zema_hive

Validate Hive documents on read and write. No TypeAdapter, no code generation.

Installation

dependencies:
  zema: ^0.5.0
  zema_hive: ^0.1.0
  hive_ce: ^2.19.3
import 'package:hive_ce/hive_ce.dart';
import 'package:zema/zema.dart';
import 'package:zema_hive/zema_hive.dart';

Quick start

final userSchema = z.object({
  'id': z.string(),
  'name': z.string().min(1),
  'email': z.string().email(),
});

final box = await Hive.openBox('users');
final userBox = box.withZema(userSchema);

// Write: validated before storage, throws ZemaHiveException on failure
await userBox.put('alice', {
  'id': 'alice',
  'name': 'Alice',
  'email': 'alice@example.com',
});

// Read: validated on retrieval, returns null if validation fails
final user = userBox.get('alice'); // Map<String, dynamic>?
print(user?['name']); // Alice

How it works

withZema(schema) wraps a Box in a ZemaBox<T> that:

  1. Runs schema.safeParse(data) on every put(). Throws ZemaHiveException if it fails, nothing is written.
  2. Runs schema.safeParse(data) on every get(). Returns null (or defaultValue) if it fails.
  3. Applies the migrate callback when get() fails validation, then re-validates and writes the result back automatically.

Extension types

Wrap the result in a Dart extension type for named field access without runtime overhead:

extension type User(Map<String, dynamic> _) {
  String get id => _['id'] as String;
  String get name => _['name'] as String;
  String get email => _['email'] as String;
}

// Schema produces Map<String, dynamic>. Wrap on the way out.
final raw = userBox.get('alice');
if (raw != null) {
  final user = User(raw);
  print(user.name); // Alice
}

// Wrap all values
for (final user in userBox.values.map(User.new)) {
  print(user.name);
}

Migration

When your schema evolves, pass a migrate callback. It is called when a stored document fails validation. The result is re-validated and written back to Hive if it passes:

final userBox = box.withZema(
  userSchemaV2,
  migrate: (rawData) {
    // v1 -> v2: add 'role' field
    if (!rawData.containsKey('role')) rawData['role'] = 'user';
    // v2 -> v3: rename 'email' -> 'emailAddress'
    if (rawData.containsKey('email') && !rawData.containsKey('emailAddress')) {
      rawData['emailAddress'] = rawData.remove('email');
    }
    return rawData;
  },
);

Make each migration idempotent (check before modifying). The callback runs once per failing document; after the migrated version is written back, subsequent reads succeed without calling migrate again.

Error handling

final userBox = box.withZema(
  userSchema,
  onParseError: (key, rawData, issues) {
    Sentry.captureMessage('Corrupt doc $key: $issues');
    return {'id': key, 'name': 'Unknown', 'email': 'unknown@example.com'};
  },
);

onParseError is called when get() fails validation and either no migrate callback is provided or migration also fails. Return a non-null value to recover, or null to fall back to defaultValue.

API reference

Method / PropertyDescription
put(key, value)Validate and write. Throws ZemaHiveException on failure.
putAll(entries)Validate all entries, then write atomically. Throws on first failure, nothing is written.
get(key)Read and validate. Apply migrate if needed. Returns null on failure.
valuesAll valid documents. Invalid entries are silently skipped.
toMap()All valid documents as Map<String, T>.
delete(key)Delete a document.
deleteAll(keys)Delete multiple documents.
clear()Delete all documents.
keysAll stored keys.
lengthNumber of stored entries.
containsKey(key)Whether a key exists.
compact()Compact the underlying Hive box.
close()Close the underlying Hive box.
Copyright © 2026