Dynamic Backend Schema Mapper

Runtime JSON Model Generator for Flutter — Automatically adapt to evolving backend JSON structures without manual model updates.

pub package License: MIT

Why This Package?

Backend APIs evolve constantly—new fields get added, old ones removed, types change. Traditional model classes break easily, requiring constant maintenance. Dynamic Backend Schema Mapper solves this by:

  • Zero Model Classes — No manual model updates needed
  • Type-Safe AccessgetString(), getInt(), getBool() with defaults
  • Auto-Adaptation — Handles backend changes automatically
  • Schema Detection — Get notified when backend structure changes
  • Deep Nesting — Fully supports nested objects and lists
  • Crash Prevention — Default values prevent null reference errors

Demo

Schema Parsing & Type-Safe Access

Dynamic Schema Parsing

Demonstrating real-time JSON parsing with type-safe getters and default values

Schema Change Detection

Schema Change Detection

Automatic detection and notification of backend schema changes

Installation

Add to your pubspec.yaml:

dependencies:
  dynamic_schema_mapper: ^0.1.0

Then run:

flutter pub get

Quick Start

Basic Usage

import 'package:dynamic_schema_mapper/dynamic_schema_mapper.dart';

// Parse any JSON response
final jsonResponse = {
  'id': 123,
  'name': 'John Doe',
  'age': 30,
  'premium': true,
  'balance': 1250.50,
};

final schema = DynamicSchema.parse(jsonResponse);

// Type-safe access with automatic defaults
final name = schema.getString('name');           // "John Doe"
final age = schema.getInt('age');                // 30
final premium = schema.getBool('premium');       // true
final balance = schema.getDouble('balance');     // 1250.50

// Non-existent keys return safe defaults
final phone = schema.getString('phone', defaultValue: 'N/A');  // "N/A"

Nested Objects

final response = {
  'user': {
    'name': 'Alice',
    'address': {
      'city': 'Springfield',
      'zipcode': 12345,
    }
  }
};

final schema = DynamicSchema.parse(response);

// Navigate nested structures
final user = schema.getNested('user');
final address = user?.getNested('address');

print(address?.getString('city'));        // "Springfield"
print(address?.getInt('zipcode'));        // 12345

// Or use dot notation
print(schema.getValueAtPath('user.address.city'));  // "Springfield"

Lists of Objects

final response = {
  'products': [
    {'id': 1, 'name': 'Laptop', 'price': 999.99},
    {'id': 2, 'name': 'Mouse', 'price': 29.99},
  ]
};

final schema = DynamicSchema.parse(response);
final products = schema.getList('products');

for (final product in products) {
  print('${product.getString('name')}: \$${product.getDouble('price')}');
}
// Output:
// Laptop: $999.99
// Mouse: $29.99

Schema Change Detection

Get notified automatically when your backend structure changes:

// Enable change detection
DynamicSchema.enableSchemaDetection((changes) {
  print('⚠️ Backend schema changed!');
  for (final change in changes) {
    print('• $change');
  }
});

// First API call
DynamicSchema.parse({'id': 1, 'name': 'Product A', 'price': 99.99});

// Backend evolves - new fields added
DynamicSchema.parse({
  'id': 2, 
  'name': 'Product B', 
  'price': 149.99,
  'category': 'Electronics',  // NEW!
  'inStock': true,            // NEW!
});

// Console output:
// ⚠️ Backend schema changed!
// • Fields Added: category
// • Fields Added: inStock

Features

Type-Safe Getters

All getters include default values to prevent crashes:

schema.getString('key', defaultValue: 'default');
schema.getInt('key', defaultValue: 0);
schema.getDouble('key', defaultValue: 0.0);
schema.getBool('key', defaultValue: false);

// Get all keys
final keys = schema.keys;  // ['id', 'name', 'age']

// Check if key exists
if (schema.hasKey('email')) {
  print(schema.getString('email'));
}

// Get all paths (including nested)
final paths = schema.getAllPaths();  
// ['user', 'user.name', 'user.address', 'user.address.city']

// Get schema structure (types only)
final structure = schema.getSchemaStructure();

Convert Back to JSON

// Pretty print
print(schema.toJsonString(pretty: true));

// Compact
final json = schema.toJsonString();

Optional Caching

Cache schemas locally for offline use or comparison:

import 'package:dynamic_schema_mapper/cache_manager.dart';

final cache = SchemaCacheManager(
  namespace: 'my_app',
  cacheDuration: Duration(hours: 24),
);

// Save schema
await cache.saveSchema('users', schema.getSchemaStructure());

// Load cached schema
final cachedSchema = await cache.loadSchema('users');

// Check if valid cache exists
if (await cache.hasValidCache('users')) {
  print('Using cached schema');
}

Screenshots

Home Screen Basic Usage Nested Objects Lists And Arrays Dashboard
Home Screen Basic JSON Parsing Nested Objects Lists And Arrays Dashboard

Architecture

dynamic_schema_mapper/
├─ lib/
│  ├─ dynamic_schema_mapper.dart     # Main API
│  └─ src/
│     ├─ schema_node.dart            # Core data structure
│     ├─ schema_parser.dart          # JSON → SchemaNode
│     ├─ schema_diff.dart            # Change detection
│     └─ cache_manager.dart          # Optional caching

Core Components

  • SchemaNode: Represents JSON values (primitives, objects, lists)
  • SchemaParser: Converts JSON to SchemaNode tree
  • SchemaDiff: Detects changes between schemas
  • CacheManager: Optional local schema storage

Real-World Example

// Complex e-commerce order
final order = DynamicSchema.parse(orderApiResponse);

// Customer info
final customer = order.getNested('customer');
print('Customer: ${customer?.getString('name')}');
print('Loyalty: ${customer?.getString('loyaltyTier')}');

// Order items
final items = order.getList('items');
for (final item in items) {
  final name = item.getString('name');
  final qty = item.getInt('quantity');
  final price = item.getDouble('unitPrice');
  print('$name: $qty × \$$price');
}

// Shipping address
final address = order.getNested('shipping')?.getNested('address');
print('Ship to: ${address?.getString('city')}, ${address?.getString('state')}');

// Payment
final payment = order.getNested('payment');
final paid = payment?.getBool('paid') ?? false;
print('Payment status: ${paid ? 'PAID' : 'PENDING'}');

Advanced Usage

Compare Two Schemas

final oldSchema = DynamicSchema.parse(oldApiResponse);
final newSchema = DynamicSchema.parse(newApiResponse);

final changes = DynamicSchema.compareSchemas(oldSchema, newSchema);

for (final change in changes) {
  print(change);
}

Debug Schema Tree

// Print entire schema structure
schema.printTree();

// Output:
// object {
//   name:
//     string: John Doe
//   age:
//     integer: 30
//   address:
//     object {
//       city:
//         string: Springfield
//     }
// }

Performance

  • Fast Parsing: Minimal overhead compared to manual models
  • Lazy Evaluation: Only processes accessed fields
  • Memory Efficient: Shared references, no duplication

Note :

Performance varies based on JSON size and structure. The package is optimized for real-world use cases where flexibility is more important than raw speed.

The package is optimized for real-world scenarios where API flexibility and zero maintenance are priorities. Performance characteristics scale well with JSON size, with lazy evaluation ensuring only accessed fields are processed.

Limitations

  • No Static Types: Fields are accessed dynamically at runtime
  • No Code Generation: Everything happens at runtime

Contributing

Contributions are welcome! If you find a bug or have a feature request, please open an issue on GitHub.

License

MIT License - see LICENSE file.

Support

Show Your Support

If this package helps your project, give it a ⭐ on GitHub!


Made with ❤️ for the Flutter community