Skip to content

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 field
final nameSchema = Schema.string(
minLength: 1,
maxLength: 100,
);
// Number field
final ageSchema = Schema.number(
min: 0,
max: 150,
);
// Boolean field
final activeSchema = Schema.boolean();
// Email field with built-in validation
final emailSchema = Schema.email();
// URL field
final websiteSchema = Schema.url();
// UUID field
final 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 strings
final tagsSchema = Schema.array(Schema.string());
// Array of objects
final commentsSchema = Schema.array(
Schema.object({
'id': Schema.id(),
'text': Schema.string(minLength: 1),
'authorId': Schema.id(),
'createdAt': Schema.number(),
})
);
// Array with size constraints
final 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 data
final todoData = {
'id': db.id(),
'text': 'Learn InstantDB schemas',
'completed': false,
'priority': 'medium',
'createdAt': DateTime.now().millisecondsSinceEpoch,
'tags': ['flutter', 'database'],
};
// Check if data is valid
final isValid = todoSchema.validate(todoData);
print('Valid: $isValid'); // true
// Get validation errors
final 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,
});
}
// Usage
Future<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 schema
final 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 database
final 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 simple
final userSchema = Schema.object({
'id': Schema.id(),
'name': Schema.string(),
'email': Schema.email(),
});
// ❌ Avoid: Over-engineering from the start
final 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 constraints
final 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 constraints
final 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 system
final 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();
}
}
// Usage
final 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: