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:
- Runs
schema.safeParse(data)on everyput(). ThrowsZemaHiveExceptionif it fails, nothing is written. - Runs
schema.safeParse(data)on everyget(). Returnsnull(ordefaultValue) if it fails. - Applies the
migratecallback whenget()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 / Property | Description |
|---|---|
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. |
values | All 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. |
keys | All stored keys. |
length | Number of stored entries. |
containsKey(key) | Whether a key exists. |
compact() | Compact the underlying Hive box. |
close() | Close the underlying Hive box. |