Schema Definition
InstantDB Flutter provides a powerful schema system for defining, validating, and enforcing data structures. Schemas ensure data consistency, provide type safety, and catch errors early in development.
Why Use Schemas?
Schemas provide several key benefits:
- ✅ Type Safety - Catch type errors at development time
- ✅ Data Validation - Ensure data meets your requirements
- ✅ Documentation - Schema serves as living documentation
- ✅ Auto-completion - Better IDE support with defined structures
- ✅ Runtime Protection - Prevent invalid data from entering your database
Basic Schema Definition
Simple Field Types
Define schemas using the Schema
class with various field types:
import 'package:instantdb_flutter/instantdb_flutter.dart';
// String fieldfinal nameSchema = Schema.string( minLength: 1, maxLength: 100,);
// Number fieldfinal ageSchema = Schema.number( min: 0, max: 150,);
// Boolean fieldfinal activeSchema = Schema.boolean();
// Email field with built-in validationfinal emailSchema = Schema.email();
// URL fieldfinal websiteSchema = Schema.url();
// UUID fieldfinal idSchema = Schema.id();
Object Schemas
Combine fields into object schemas:
final userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(minLength: 1, maxLength: 100), 'email': Schema.email(), 'age': Schema.number(min: 0, max: 150), 'active': Schema.boolean(), 'website': Schema.url().optional(), // Optional field}, required: ['id', 'name', 'email']); // Specify required fields
Array Schemas
Define arrays of specific types:
// Array of stringsfinal tagsSchema = Schema.array(Schema.string());
// Array of objectsfinal commentsSchema = Schema.array( Schema.object({ 'id': Schema.id(), 'text': Schema.string(minLength: 1), 'authorId': Schema.id(), 'createdAt': Schema.number(), }));
// Array with size constraintsfinal skillsSchema = Schema.array( Schema.string(), minLength: 1, // At least one skill maxLength: 10, // At most 10 skills);
Schema Validation
Validating Data
Use schemas to validate data before storing:
final todoSchema = Schema.object({ 'id': Schema.id(), 'text': Schema.string(minLength: 1, maxLength: 500), 'completed': Schema.boolean(), 'priority': Schema.string().oneOf(['low', 'medium', 'high']), 'createdAt': Schema.number(), 'tags': Schema.array(Schema.string()).optional(),});
// Validate datafinal todoData = { 'id': db.id(), 'text': 'Learn InstantDB schemas', 'completed': false, 'priority': 'medium', 'createdAt': DateTime.now().millisecondsSinceEpoch, 'tags': ['flutter', 'database'],};
// Check if data is validfinal isValid = todoSchema.validate(todoData);print('Valid: $isValid'); // true
// Get validation errorsfinal errors = todoSchema.getErrors(todoData);if (errors.isNotEmpty) { print('Validation errors: $errors');}
Validation in Practice
Create a validation helper for your entities:
class TodoValidator { static final schema = Schema.object({ 'id': Schema.id(), 'text': Schema.string(minLength: 1, maxLength: 500), 'completed': Schema.boolean(), 'priority': Schema.string().oneOf(['low', 'medium', 'high']), 'createdAt': Schema.number(), 'userId': Schema.id(), 'tags': Schema.array(Schema.string()).optional(), }, required: ['id', 'text', 'completed', 'priority', 'createdAt', 'userId']);
static ValidationResult validate(Map<String, dynamic> data) { final isValid = schema.validate(data); final errors = schema.getErrors(data);
return ValidationResult( isValid: isValid, errors: errors, ); }
static Map<String, dynamic> sanitize(Map<String, dynamic> data) { // Remove fields not in schema final validKeys = schema.properties.keys.toSet(); final sanitized = <String, dynamic>{};
data.forEach((key, value) { if (validKeys.contains(key)) { sanitized[key] = value; } });
return sanitized; }}
class ValidationResult { final bool isValid; final List<String> errors;
const ValidationResult({ required this.isValid, required this.errors, });}
// UsageFuture<void> createTodoSafely(Map<String, dynamic> todoData) async { final validation = TodoValidator.validate(todoData);
if (!validation.isValid) { throw InstantException( message: 'Invalid todo data: ${validation.errors.join(', ')}', code: 'validation_error', ); }
final sanitizedData = TodoValidator.sanitize(todoData);
await db.transact([ ...db.create('todos', sanitizedData), ]);}
Entity Schema Builder
For complex applications, use the InstantSchemaBuilder
to define schemas for multiple entity types:
final userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(minLength: 1, maxLength: 100), 'email': Schema.email(), 'role': Schema.string().oneOf(['user', 'admin', 'moderator']), 'createdAt': Schema.number(), 'profile': Schema.object({ 'bio': Schema.string(maxLength: 500).optional(), 'avatar': Schema.url().optional(), 'preferences': Schema.object({ 'theme': Schema.string().oneOf(['light', 'dark', 'auto']), 'notifications': Schema.boolean(), }), }).optional(),});
final postSchema = Schema.object({ 'id': Schema.id(), 'title': Schema.string(minLength: 1, maxLength: 200), 'content': Schema.string(minLength: 1), 'authorId': Schema.id(), 'published': Schema.boolean(), 'publishedAt': Schema.number().optional(), 'tags': Schema.array(Schema.string()), 'metadata': Schema.object({ 'viewCount': Schema.number(min: 0), 'likeCount': Schema.number(min: 0), 'featured': Schema.boolean(), }),});
// Build complete schemafinal appSchema = InstantSchemaBuilder() .addEntity('users', userSchema) .addEntity('posts', postSchema) .addEntity('comments', Schema.object({ 'id': Schema.id(), 'postId': Schema.id(), 'authorId': Schema.id(), 'text': Schema.string(minLength: 1, maxLength: 1000), 'createdAt': Schema.number(), 'approved': Schema.boolean(), })) .build();
// Use schema with databasefinal db = await InstantDB.init( appId: 'your-app-id', schema: appSchema, // Optional: Apply schema validation);
Advanced Schema Patterns
Conditional Validation
Create schemas with conditional validation:
final eventSchema = Schema.object({ 'id': Schema.id(), 'type': Schema.string().oneOf(['meeting', 'deadline', 'reminder']), 'title': Schema.string(minLength: 1), 'date': Schema.number(),
// Conditional fields based on type 'meetingDetails': Schema.object({ 'location': Schema.string(), 'attendees': Schema.array(Schema.id()), }).when('type', 'meeting'), // Only required when type is 'meeting'
'reminderDetails': Schema.object({ 'reminderTime': Schema.number(), 'recurring': Schema.boolean(), }).when('type', 'reminder'),});
Custom Validation
Define custom validation logic:
final passwordSchema = Schema.string() .custom((value) { if (value.length < 8) { return 'Password must be at least 8 characters'; }
if (!RegExp(r'[A-Z]').hasMatch(value)) { return 'Password must contain uppercase letter'; }
if (!RegExp(r'[a-z]').hasMatch(value)) { return 'Password must contain lowercase letter'; }
if (!RegExp(r'[0-9]').hasMatch(value)) { return 'Password must contain number'; }
if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) { return 'Password must contain special character'; }
return null; // Valid });
final userRegistrationSchema = Schema.object({ 'email': Schema.email(), 'password': passwordSchema, 'confirmPassword': Schema.string(),}).custom((data) { if (data['password'] != data['confirmPassword']) { return 'Passwords do not match'; } return null;});
Nested Object Validation
Handle complex nested structures:
final organizationSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(minLength: 1), 'settings': Schema.object({ 'billing': Schema.object({ 'plan': Schema.string().oneOf(['free', 'pro', 'enterprise']), 'subscriptionId': Schema.string().optional(), 'trialEndsAt': Schema.number().optional(), }), 'features': Schema.object({ 'maxUsers': Schema.number(min: 1), 'storageLimit': Schema.number(min: 0), // in GB 'apiAccess': Schema.boolean(), 'integrations': Schema.array(Schema.string()), }), 'security': Schema.object({ 'twoFactorRequired': Schema.boolean(), 'passwordPolicy': Schema.object({ 'minLength': Schema.number(min: 8, max: 128), 'requireUppercase': Schema.boolean(), 'requireNumbers': Schema.boolean(), 'requireSymbols': Schema.boolean(), }), 'sessionTimeout': Schema.number(min: 300, max: 86400), // 5 min to 24 hours }), }),});
Schema Integration
With Queries
Schemas help ensure query results match expected structures:
final userQuerySchema = Schema.object({ 'users': Schema.array(userSchema),});
Future<List<User>> getUsers() async { final result = await db.queryOnce({'users': {}});
// Validate query result if (!userQuerySchema.validate(result.data)) { throw InstantException( message: 'Invalid query result structure', code: 'schema_error', ); }
return (result.data!['users'] as List) .map((json) => User.fromJson(json)) .toList();}
With Transactions
Validate data before transactions:
class SchemaAwareService { final InstantDB db; final Schema schema;
SchemaAwareService(this.db, this.schema);
Future<void> create(String entityType, Map<String, dynamic> data) async { // Validate against schema final entitySchema = schema.getEntity(entityType); if (entitySchema != null && !entitySchema.validate(data)) { final errors = entitySchema.getErrors(data); throw InstantException( message: 'Schema validation failed: ${errors.join(', ')}', code: 'validation_error', ); }
await db.transact([ ...db.create(entityType, data), ]); }
Future<void> update(String entityId, Map<String, dynamic> data) async { // Validate partial update data final updates = _validatePartialUpdate(data);
await db.transact([ db.update(entityId, updates), ]); }
Map<String, dynamic> _validatePartialUpdate(Map<String, dynamic> data) { // Custom validation logic for partial updates final validated = <String, dynamic>{};
data.forEach((key, value) { if (_isValidField(key, value)) { validated[key] = value; } });
return validated; }
bool _isValidField(String key, dynamic value) { // Implement field-level validation return true; }}
Schema Versioning
Handle schema evolution over time:
class SchemaVersionManager { static const int currentVersion = 2;
static Schema getSchemaForVersion(int version) { switch (version) { case 1: return _getV1Schema(); case 2: return _getV2Schema(); default: throw ArgumentError('Unsupported schema version: $version'); } }
static Schema _getV1Schema() { return Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(), }); }
static Schema _getV2Schema() { return Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(), 'profile': Schema.object({ 'bio': Schema.string().optional(), 'avatar': Schema.url().optional(), }).optional(), 'version': Schema.number(), }); }
static Map<String, dynamic> migrateData( Map<String, dynamic> data, int fromVersion, int toVersion, ) { var migrated = Map<String, dynamic>.from(data);
for (int v = fromVersion; v < toVersion; v++) { migrated = _migrateBetweenVersions(migrated, v, v + 1); }
return migrated; }
static Map<String, dynamic> _migrateBetweenVersions( Map<String, dynamic> data, int from, int to, ) { switch ('$from->$to') { case '1->2': return { ...data, 'profile': null, 'version': 2, }; default: return data; } }}
Best Practices
1. Start Simple
Begin with basic schemas and add complexity as needed:
// ✅ Good: Start simplefinal userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(),});
// ❌ Avoid: Over-engineering from the startfinal userSchema = Schema.object({ 'id': Schema.id(), 'name': Schema.string().custom(...).transform(...), 'email': Schema.email().custom(...).when(...), // ... many complex validations});
2. Use Meaningful Constraints
Apply constraints that reflect real business rules:
// ✅ Good: Meaningful constraintsfinal productSchema = Schema.object({ 'name': Schema.string(minLength: 1, maxLength: 100), 'price': Schema.number(min: 0.01, max: 999999.99), 'category': Schema.string().oneOf(['electronics', 'clothing', 'books']), 'inStock': Schema.boolean(),});
// ❌ Avoid: Arbitrary or missing constraintsfinal productSchema = Schema.object({ 'name': Schema.string(), // No length limits 'price': Schema.number(), // Could be negative 'category': Schema.string(), // Any string allowed});
3. Document Your Schemas
Add comments and documentation:
/// User entity schema/// Represents a registered user in the systemfinal userSchema = Schema.object({ 'id': Schema.id(), // UUID generated by InstantDB 'name': Schema.string(minLength: 1, maxLength: 100), // Display name 'email': Schema.email(), // Must be valid email address 'role': Schema.string().oneOf(['user', 'admin']), // User permission level 'createdAt': Schema.number(), // Unix timestamp 'lastLoginAt': Schema.number().optional(), // Unix timestamp, null if never logged in});
4. Validate Early and Often
Validate data at multiple points:
class DataService { // Validate on input Future<void> createUser(Map<String, dynamic> userData) async { _validateUserData(userData);
await db.transact([ ...db.create('users', userData), ]); }
// Validate on output Future<User> getUser(String userId) async { final result = await db.queryOnce({ 'users': {'where': {'id': userId}}, });
final userData = result.data?['users']?.first; if (userData != null) { _validateUserData(userData); // Ensure data integrity return User.fromJson(userData); }
throw Exception('User not found'); }
void _validateUserData(Map<String, dynamic> data) { if (!userSchema.validate(data)) { throw InstantException( message: 'Invalid user data', code: 'validation_error', ); } }}
5. Handle Validation Errors Gracefully
Provide helpful error messages:
class UserFriendlyValidator { static String formatValidationErrors(List<String> errors) { if (errors.isEmpty) return '';
final formatted = errors.map((error) { // Convert technical errors to user-friendly messages if (error.contains('minLength')) { return 'This field is too short'; } else if (error.contains('email')) { return 'Please enter a valid email address'; } else if (error.contains('required')) { return 'This field is required'; } return error; }).join(', ');
return formatted; }}
Performance Considerations
Schema Caching
Cache compiled schemas for better performance:
class SchemaCache { static final Map<String, Schema> _cache = {};
static Schema getOrCreate(String key, Schema Function() factory) { return _cache.putIfAbsent(key, factory); }
static void clear() { _cache.clear(); }}
// Usagefinal userSchema = SchemaCache.getOrCreate('user', () { return Schema.object({ 'id': Schema.id(), 'name': Schema.string(), 'email': Schema.email(), });});
Lazy Validation
Only validate when necessary:
class LazyValidatedData { final Map<String, dynamic> _data; final Schema _schema; bool? _isValid; List<String>? _errors;
LazyValidatedData(this._data, this._schema);
bool get isValid { _isValid ??= _schema.validate(_data); return _isValid!; }
List<String> get errors { _errors ??= _schema.getErrors(_data); return _errors!; }
Map<String, dynamic> get data => _data;}
Next Steps
Now that you understand schemas, explore related topics:
- Database Setup - Initialize your database with schemas
- Queries - Type-safe queries with schema validation
- Transactions - Schema-validated mutations
- Advanced Patterns - Schema migration strategies