Parsable
Type-safe map parsing for Dart. Simplifies extracting values from Map<String, dynamic> with automatic type conversion and nested object support.
Features
- Type-safe value extraction - Get values with compile-time type safety
- Custom type parsing - Parse any type with custom parser functions (String → DateTime, int, enums, etc.)
- Automatic type conversions - Seamless
int↔doubleconversions - Nested object support - Parse complex object hierarchies with ease
- List parsing - Easy parsing of lists with
getList()method - Equatable integration - Built-in value equality comparison
- Configurable error handling - Customize how parsing errors are handled
- Null safety - Full null safety support out of the box
- Zero boilerplate - Clean, readable model definitions
Installation
Add parsable to your pubspec.yaml:
dependencies:
parsable: ^0.2.0
Then run:
dart pub get
Usage
Basic Example
Create a model by extending Parsable and define getters using the get<T>() method:
import 'package:parsable/parsable.dart';
class User extends Parsable {
const User({required super.data});
String? get name => get('name');
int? get age => get('age');
String? get email => get('email');
bool? get isActive => get('isActive');
factory User.fromMap(Map<String, dynamic> map) => User(data: map);
}
void main() {
final userData = {
'name': 'Alice',
'age': 28,
'email': 'alice@example.com',
'isActive': true,
};
final user = User.fromMap(userData);
print(user.name); // Alice
print(user.age); // 28
print(user.isActive); // true
}
Default Values
Use the null-aware operator ?? to provide default values:
class User extends Parsable {
const User({required super.data});
String get name => get('name') ?? 'Unknown';
int get age => get('age') ?? 0;
bool get isActive => get('isActive') ?? false;
factory User.fromMap(Map<String, dynamic> map) => User(data: map);
}
Nested Objects
Parse nested objects by providing a parser function:
class Address extends Parsable {
const Address({required super.data});
String? get street => get('street');
String? get city => get('city');
String? get zipCode => get('zipCode');
String? get country => get('country');
factory Address.fromMap(Map<String, dynamic> map) => Address(data: map);
}
class User extends Parsable {
const User({required super.data});
String? get name => get('name');
int? get age => get('age');
Address? get address => get('address', parser: Address.fromMap);
factory User.fromMap(Map<String, dynamic> map) => User(data: map);
}
void main() {
final userData = {
'name': 'Bob',
'age': 35,
'address': {
'street': '123 Main St',
'city': 'Springfield',
'zipCode': '12345',
'country': 'USA',
},
};
final user = User.fromMap(userData);
print(user.name); // Bob
print(user.address?.city); // Springfield
print(user.address?.zipCode); // 12345
}
Parsing Lists
Parse lists of objects using the getList() method:
class Comment extends Parsable {
const Comment({required super.data});
String? get author => get('author');
String? get text => get('text');
factory Comment.fromMap(Map<String, dynamic> map) => Comment(data: map);
}
class Post extends Parsable {
const Post({required super.data});
String? get title => get('title');
String? get content => get('content');
// Parse list of comments with getList
List<Comment> get comments => getList('comments', parser: Comment.fromMap) ?? [];
factory Post.fromMap(Map<String, dynamic> map) => Post(data: map);
}
void main() {
final postData = {
'title': 'Hello World',
'content': 'This is my first post',
'comments': [
{'author': 'Alice', 'text': 'Great post!'},
{'author': 'Bob', 'text': 'Thanks for sharing!'},
],
};
final post = Post.fromMap(postData);
print(post.title); // Hello World
print(post.comments.length); // 2
print(post.comments[0].author); // Alice
}
Custom Type Parsing
Parse any type using custom parser functions. Thanks to Dart's type inference, you don't need to explicitly specify type parameters:
class Event extends Parsable {
const Event({required super.data});
String? get name => get('name');
// Parse String to DateTime
DateTime? get startDate => get('startDate',
parser: (String val) => DateTime.parse(val)
);
// Parse String to int
int? get attendeeCount => get('attendeeCount',
parser: (String val) => int.parse(val)
);
// Parse String to custom enum
EventStatus get status => get('status',
parser: (String val) => EventStatus.fromString(val)
) ?? EventStatus.pending;
// Parse list of date strings to list of DateTime objects
List<DateTime> get eventDates => getList('eventDates',
parser: (String val) => DateTime.parse(val)
) ?? [];
factory Event.fromMap(Map<String, dynamic> map) => Event(data: map);
}
enum EventStatus {
pending,
confirmed,
cancelled;
static EventStatus fromString(String value) {
return EventStatus.values.firstWhere(
(e) => e.name == value,
orElse: () => EventStatus.pending,
);
}
}
void main() {
final eventData = {
'name': 'Tech Conference 2024',
'startDate': '2024-06-15T09:00:00Z',
'attendeeCount': '150',
'status': 'confirmed',
'eventDates': [
'2024-06-15T09:00:00Z',
'2024-06-16T09:00:00Z',
'2024-06-17T09:00:00Z',
],
};
final event = Event.fromMap(eventData);
print(event.name); // Tech Conference 2024
print(event.startDate); // 2024-06-15 09:00:00.000Z
print(event.attendeeCount); // 150
print(event.status); // EventStatus.confirmed
print(event.eventDates.length); // 3
}
The parser function receives the exact type you specify (e.g., String) and the compiler ensures type safety at compile time. If the value in the map doesn't match the expected type, an error will be logged and null will be returned.
Complex Example with Multiple Nested Objects
class Product extends Parsable {
const Product({required super.data});
String? get id => get('id');
String? get name => get('name');
double? get price => get('price');
factory Product.fromMap(Map<String, dynamic> map) => Product(data: map);
}
class OrderItem extends Parsable {
const OrderItem({required super.data});
Product? get product => get('product', parser: Product.fromMap);
int? get quantity => get('quantity');
double? get total => (product?.price ?? 0) * (quantity ?? 0);
factory OrderItem.fromMap(Map<String, dynamic> map) => OrderItem(data: map);
}
class Order extends Parsable {
const Order({required super.data});
String? get orderId => get('orderId');
// Parse ISO 8601 date string to DateTime
DateTime? get orderDate => get('orderDate',
parser: (String val) => DateTime.parse(val)
);
Address? get shippingAddress => get('shippingAddress', parser: Address.fromMap);
List<OrderItem> get items => getList('items', parser: OrderItem.fromMap) ?? [];
factory Order.fromMap(Map<String, dynamic> map) => Order(data: map);
}
Automatic Numeric Conversions
By default, parsable automatically converts between int and double:
final data = {'count': 42}; // int value
final obj = MyParsable(data: data);
double? value = obj.get('count'); // Returns 42.0 (automatic int→double conversion)
To disable this behavior:
Parsable.handleNumericConversions(false);
Custom Error Handling
Customize how parsing errors are handled:
// Throw exceptions on errors
Parsable.setOnParseError((message) {
throw FormatException(message);
});
// Use a custom logger
Parsable.setOnParseError((message) {
myLogger.error(message);
});
// Silent mode (ignore errors)
Parsable.setOnParseError((message) {});
Equatable Integration
All Parsable objects automatically support value equality:
final user1 = User.fromMap({'name': 'Alice', 'age': 28});
final user2 = User.fromMap({'name': 'Alice', 'age': 28});
final user3 = User.fromMap({'name': 'Bob', 'age': 30});
print(user1 == user2); // true
print(user1 == user3); // false
Converting Back to Map
final user = User.fromMap({'name': 'Alice', 'age': 28});
final map = user.toMap(); // Returns the original Map<String, dynamic>
Common Use Cases
JSON API Responses
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiUser extends Parsable {
const ApiUser({required super.data});
String? get id => get('id');
String? get username => get('username');
String? get email => get('email');
factory ApiUser.fromMap(Map<String, dynamic> map) => ApiUser(data: map);
}
Future<ApiUser> fetchUser(String userId) async {
final response = await http.get(Uri.parse('https://api.example.com/users/$userId'));
final json = jsonDecode(response.body) as Map<String, dynamic>;
return ApiUser.fromMap(json);
}
Configuration Files
class AppConfig extends Parsable {
const AppConfig({required super.data});
String get apiUrl => get('apiUrl') ?? 'https://api.example.com';
int get timeout => get('timeout') ?? 30;
bool get enableLogging => get('enableLogging') ?? false;
String? get apiKey => get('apiKey');
factory AppConfig.fromMap(Map<String, dynamic> map) => AppConfig(data: map);
}
Local Storage / SharedPreferences
class UserPreferences extends Parsable {
const UserPreferences({required super.data});
String get theme => get('theme') ?? 'light';
String get language => get('language') ?? 'en';
bool get notificationsEnabled => get('notificationsEnabled') ?? true;
factory UserPreferences.fromMap(Map<String, dynamic> map) =>
UserPreferences(data: map);
}
Why Parsable?
Traditional approaches to parsing maps in Dart often involve:
- Manual null checking for every field
- Verbose casting:
data['name'] as String? - Repetitive error handling
- Difficult nested object parsing
Parsable eliminates this boilerplate while providing:
- Clean, readable code
- Type safety
- Automatic type conversions
- Easy nested object handling
- Consistent error handling
Additional Information
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Issues
If you encounter any issues or have feature requests, please file them in the issue tracker.
License
This project is licensed under the MIT License - see the LICENSE file for details.