json_annotation_tools 0.1.7
json_annotation_tools: ^0.1.7 copied to clipboard
Transform cryptic JSON parsing errors into crystal-clear, actionable error messages. Perfect companion to json_annotation.
JSON Annotation Tools ๐ ๏ธ #
Stop wrestling with cryptic JSON parsing errors! This package provides powerful debugging tools and safe parsing utilities that make JSON deserialization errors crystal clear and easy to fix.
[Error Comparison Demo] Before vs After: See how error messages transform from cryptic to crystal clear
๐ฏ Why You Need This #
Ever seen this frustrating error?
type 'String' is not a subtype of type 'int'
And spent hours figuring out which field in your JSON caused it? Those days are over.
Before vs After #
โ Before (Standard JSON parsing):
// Unclear error: "type 'String' is not a subtype of type 'int'"
// Which field? What value? Who knows! ๐คทโโ๏ธ
User.fromJson(json);
โ After (With JSON Annotation Tools):
// Crystal clear error message:
// "โ Error parsing key 'age': expected int, but got String. Value: "25""
User.fromJsonSafe(json);
[Complete Demo Flow] ๐ฌ See the complete debugging experience in action
๐ Features #
๐ฏ Revolutionary Code Generation #
- ๐ช Zero-Hassle Setup: Just add
@SafeJsonParsing()annotation - that's it! - ๐ Automatic Generation: Build runner creates optimized safe parsing methods
- โก Best Performance: Generated code is as fast as hand-written safe parsing
- ๐ง Highly Customizable: Field-level annotations for enhanced error context
โก Manual Convenience Methods #
- ๐ฏ Clean Convenience Methods:
json.getSafeInt(),getSafeString(),getSafeBool()and more! - ๐ No More Manual Parsers: Gone are the days of
(v) => v as inteverywhere - โก Best Performance: Same speed as manual parsing, but way cleaner code
- ๐ง Comprehensive Coverage: 12+ convenience methods for all common types
๐ Crystal-Clear Error Messages #
- ๐ Pinpoint Error Location: Know exactly which JSON field caused the parsing error
- ๐ Detailed Error Messages: See the expected type, actual type, and problematic value
- ๐ง Smart Suggestions: AI-powered field name suggestions for typos and mismatches
- ๐ Copy-Paste Solutions: Ready-to-use code fixes for every error scenario
๐ก๏ธ Bulletproof Parsing #
- ๐ก๏ธ Type Safety: Catch type mismatches before they crash your app
- ๐ซ Missing Field Detection: Clear warnings for missing required fields
- ๐ Seamless Integration: Works perfectly with
json_annotationandjson_serializable - โก Zero Performance Impact: Only active during parsing errors
- ๐ Cross-Platform: Works on iOS, Android, Web, macOS, Windows, Linux
๐ฑ Interactive Demo App #
Experience the power of enhanced error messages with our interactive demo app:
Try all features live on Android, iOS, or Web!
What You'll See: #
- ๐จ Real Error Comparisons: Before vs after side-by-side
- ๐ง Copy-Paste Solutions: Ready-to-use code fixes
- ๐ฏ Smart Suggestions: AI-powered field name matching
- ๐ Advanced Diagnostics: Property mapping analysis
- โก Live Testing: Try different JSON scenarios instantly
๐ฆ Installation #
Add to your pubspec.yaml:
dependencies:
json_annotation_tools: ^0.1.7 # ๐ Latest version!
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.12 # For @SafeJsonParsing() code generation
json_serializable: ^6.8.0
๐ฎ Quick Start #
๐ Zero-Hassle Code Generation (Recommended) #
Just add one annotation and get automatic safe parsing with enhanced error messages! โจ
Step 0: Bootstrap with the CLI (recommended)
# Adds @SafeJsonParsing() and part 'model.safe_json_parsing.g.dart';
dart run json_annotation_tools init
# Preview without writing changes
dart run json_annotation_tools init --dry-run
This scans your lib/ folder for @JsonSerializable models, inserts the missing
@SafeJsonParsing() annotation, and ensures the extra part directive is present
so you can jump straight to code generation.
Step 1: Annotate Your Model
import 'package:json_annotation/json_annotation.dart';
import 'package:json_annotation_tools/json_annotation_tools.dart';
part 'user.g.dart';
part 'user.safe_json_parsing.g.dart'; // ๐ฅ This will be generated!
@JsonSerializable()
@SafeJsonParsing() // โ Magic happens with just this line!
class User {
final int id;
final String name;
final int age;
final String? email; // Nullable fields handled automatically
User({required this.id, required this.name, required this.age, this.email});
// Standard json_serializable methods (unchanged)
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
// ๐ AUTO-GENERATED: UserSafeJsonParsing.fromJsonSafe(json) method with enhanced errors!
}
Step 2: Create build.yaml (one-time setup)
# build.yaml (in your project root)
targets:
$default:
builders:
json_annotation_tools|safe_json_parsing:
enabled: true
json_serializable|json_serializable:
enabled: true
Step 3: Generate Code
# Generate the safe parsing methods
flutter packages pub run build_runner build
# or
dart run build_runner build
Step 4: Use Enhanced Parsing & See Error Messages
final problematicJson = {
'id': 'not-a-number', // โ Should be int, got String
'name': 'John Doe',
'age': 25,
'email': 'john@example.com',
};
try {
// ๐ Use the auto-generated safe method
final user = UserSafeJsonParsing.fromJsonSafe(problematicJson);
print('Success: $user');
} catch (e) {
// ๐ Enhanced error message appears here!
print('Enhanced Error: $e');
/* Output:
๐จ OOPS! There's a problem with your JSON data:
๐ EXACT PROBLEM DIAGNOSIS:
โ Field 'id' has the wrong data type
๐ TYPE COMPARISON:
Expected: int (whole number)
Got: String (text)
Value: "not-a-number"
๐ง How to fix this (3 easy options):
1. Fix your API to return: {"id": 123}
2. Update your model: final String id;
3. Add conversion: int.tryParse(json['id'])
*/
}
Want zero call-site changes?
Create a single helper or service wrapper that always funnels through the generated safe method:
class UserParser {
static User parse(Map<String, dynamic> json) =>
UserSafeJsonParsing.fromJsonSafe(json);
}
// Use everywhere else
final user = UserParser.parse(apiResponse);
This keeps your UI/business layers unchanged while guaranteeing enhanced diagnostics.
๐ฏ Key Benefits:
- ๐ช Zero Manual Work: Just add
@SafeJsonParsing()annotation - ๐ Auto-Generated:
UserSafeJsonParsing.fromJsonSafe()method created automatically - โก Best Performance: Generated code is as fast as hand-written safe parsing
- ๐ Enhanced Errors: Crystal-clear error messages built into generated method
- ๐ง Production Ready: Perfect for error logging (Crashlytics/Sentry)
โก Alternative: Manual Convenience Extensions #
Transform your JSON parsing from hassle to happiness with super clean convenience methods:
import 'package:json_annotation_tools/json_annotation_tools.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final int age;
final String? email;
User({required this.id, required this.name, required this.age, this.email});
// Standard json_serializable methods (unchanged)
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
// ๐ ENHANCED: Safe parsing with crystal-clear error messages
factory User.fromJsonSafe(Map<String, dynamic> json) {
return User(
id: json.getSafeInt('id'), // Clean convenience methods!
name: json.getSafeString('name'), // No more manual parsers!
age: json.getSafeInt('age'), // Automatic type conversion!
email: json.getNullableSafeString('email'), // Nullable support!
);
}
}
Before vs After:
// ๐ค OLD WAY (Manual hassle):
id: json.getSafe('id', (v) => v as int),
name: json.getSafe('name', (v) => v as String),
// ๐ NEW WAY (Clean convenience):
id: json.getSafeInt('id'), // So much cleaner!
name: json.getSafeString('name'), // Zero hassle!
๐ง Alternative: Custom Parsers (For advanced control) #
If you need maximum control with custom parsing logic:
import 'package:json_annotation_tools/json_annotation_tools.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final int age;
User({required this.id, required this.name, required this.age});
// Standard method
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// Manual safe method with extensions
factory User.fromJsonSafe(Map<String, dynamic> json) {
return User(
id: json.getSafeInt('id'), // Convenience methods
name: json.getSafeString('name'),
age: json.getSafeInt('age'),
);
}
}
3. Handle problematic JSON gracefully #
final problematicJson = {
'id': 123,
'name': 'John Doe',
'age': '25', // Oops! String instead of int
};
try {
final user = User.fromJsonSafe(problematicJson);
} catch (e) {
print(e);
// Output: โ Error parsing key 'age': expected int, but got String. Value: "25"
// Now you know exactly what to fix!
}
๐งฐ Command-Line Setup Assistant #
Make onboarding effortless with the bundled CLI:
dart run json_annotation_tools init
- Annotates automatically โ adds
@SafeJsonParsing()next to every@JsonSerializablemodel. - Fixes missing part files โ ensures
part 'model.safe_json_parsing.g.dart';is present. - Dry-run friendly โ append
--dry-runto preview the diff. - Verbose mode โ use
--verboseto list already compliant files.
๐ก Tip: add this to a pre-commit hook or CI check so new models always ship with safe parsing baked in.
๐ฏ Real-World Examples #
๐ How to See Enhanced Error Messages #
The most common question: "I added @SafeJsonParsing(), but how do I see the enhanced error messages?"
Answer: Use try-catch with the generated method!
// โ WRONG: Using standard method (cryptic errors)
final user = User.fromJson(problematicJson); // Type 'String' is not a subtype...
// โ
CORRECT: Using generated safe method (enhanced errors)
try {
final user = UserSafeJsonParsing.fromJsonSafe(problematicJson);
print('Success: $user');
} catch (e) {
print('๐ Full Enhanced Error:');
print(e.toString()); // ๐ See complete detailed diagnosis here!
// ๐ญ In production, log this to your error service:
// crashlytics.recordError(e, null);
// logger.error('JSON parsing failed: ${e.toString()}');
}
๐ Production API Service Pattern #
// Example API service using @SafeJsonParsing()
class UserService {
Future<User> fetchUser(int id) async {
try {
final response = await dio.get('/api/users/$id');
// ๐ Use auto-generated safe method with enhanced errors
return UserSafeJsonParsing.fromJsonSafe(response.data);
} catch (e) {
// ๐ Enhanced error messages help debug API issues 10x faster
logger.error('User parsing failed for ID $id: ${e.toString()}');
// ๐ฏ Show user-friendly message
throw UserFetchException('Unable to load user data');
}
}
}
๐ง Advanced Field-Level Configuration #
@JsonSerializable()
@SafeJsonParsing(
validateRequiredKeys: true, // Check all keys exist first
methodName: 'parseProductSafe' // Custom method name
)
class Product {
final String id;
@SafeJsonField(
description: 'Product price in USD',
expectedFormat: 'Positive number (e.g., 19.99)',
commonValues: ['9.99', '19.99', '29.99'],
)
final double price;
@JsonKey(name: 'is_available')
@SafeJsonField(
description: 'Product availability status',
commonValues: ['true', 'false'],
)
final bool isAvailable;
// ๐ AUTO-GENERATED: ProductSafeJsonParsing.parseProductSafe(json)
// Enhanced errors include field descriptions and common values!
}
API Response Debugging #
// When your API suddenly returns unexpected data types
final apiResponse = {
'user_id': '12345', // Should be int, but API returned string
'balance': 'null', // Should be double, but got string "null"
'premium': 1, // Should be bool, but got int
};
// Get clear, actionable error messages:
try {
final user = UserSafeJsonParsing.fromJsonSafe(apiResponse);
} catch (e) {
print(e);
/* Enhanced Output:
๐จ OOPS! There's a problem with your JSON data:
๐ EXACT PROBLEM DIAGNOSIS:
โ Field 'user_id' has the wrong data type
๐ TYPE COMPARISON:
Expected: int (whole number)
Got: String (text)
Value: "12345"
๐ง How to fix this (copy-paste ready):
1. Fix your API to return: {"user_id": 12345}
2. Update your model: final String user_id;
3. Add conversion: int.tryParse(json['user_id'])
*/
// Now you can immediately contact the backend team with specific details!
}
Nullable Fields Made Easy #
factory User.fromJsonSafe(Map<String, dynamic> json) {
return User(
id: json.getSafe('id', (v) => v as int),
name: json.getSafe('name', (v) => v as String),
age: json.getSafe('age', (v) => v as int),
// Optional fields with safe parsing
email: json.getNullableSafe('email', (v) => v as String),
phoneNumber: json.getNullableSafe('phone', (v) => v as String),
);
}
Complex Type Parsing #
// Parse nested objects and lists safely
factory Order.fromJsonSafe(Map<String, dynamic> json) {
return Order(
id: json.getSafe('id', (v) => v as String),
items: json.getSafe('items', (v) =>
(v as List).map((item) => OrderItem.fromJsonSafe(item)).toList()
),
totalAmount: json.getSafe('total', (v) => (v as num).toDouble()),
createdAt: json.getSafe('created_at', (v) => DateTime.parse(v as String)),
);
}
๐ง Advanced Usage #
Custom Parser Functions #
// Create reusable parsers for common patterns
T parseEnum<T>(List<T> values, dynamic value) {
final stringValue = value as String;
return values.firstWhere(
(e) => e.toString().split('.').last == stringValue,
orElse: () => throw FormatException('Invalid enum value: $stringValue'),
);
}
// Use in your models
factory UserStatus.fromJsonSafe(Map<String, dynamic> json) {
return UserStatus(
status: json.getSafe('status', (v) => parseEnum(StatusType.values, v)),
);
}
Debugging Production Issues #
// Add logging to track parsing issues in production
factory User.fromJsonSafe(Map<String, dynamic> json) {
try {
return User(
id: json.getSafe('id', (v) => v as int),
name: json.getSafe('name', (v) => v as String),
age: json.getSafe('age', (v) => v as int),
);
} catch (e) {
// Log the error with full context for debugging
FirebaseCrashlytics.instance.recordError(
e,
null,
fatal: false,
information: ['JSON: ${jsonEncode(json)}'],
);
rethrow;
}
}
๐จ Best Practices #
- Keep Both Methods: Maintain both
fromJsonandfromJsonSafefor backward compatibility - Use in Development: Use
fromJsonSafeduring development and testing - Production Strategy: Consider using
fromJsonSafein production for critical models - Error Handling: Always wrap safe parsing in try-catch blocks
- Logging: Log parsing errors with full context for debugging
๐ Works Great With Your Stack #
Perfect integration with Dio, Retrofit, and json_annotation - use the same safe parsing methods with your existing API client setup.
๐ฎ Usage Examples #
Code Generation Example #
@JsonSerializable()
@SafeJsonParsing(
validateRequiredKeys: true, // Validate all keys exist first
methodName: 'parseJsonSafe' // Custom method name
)
class Product {
@SafeJsonField(
description: 'Product price in USD',
expectedFormat: 'Positive number (e.g., 19.99)',
commonValues: ['9.99', '19.99', '29.99'],
)
final double price;
@JsonKey(name: 'is_available')
final bool isAvailable;
// Constructor and standard methods...
// ๐ AUTO-GENERATED: Product.parseJsonSafe(json) with enhanced errors!
}
๐ CLI Command Cheat Sheet #
# Auto-annotate @JsonSerializable models with @SafeJsonParsing()
flutter pub run json_annotation_tools init
# Preview changes without writing them
flutter pub run json_annotation_tools init --dry-run
# Regenerate JSON + safe parsing code
flutter pub run build_runner build --delete-conflicting-outputs
# Optional helper to demo the enhanced error output
dart run tool/demo_safe_json_error.dart
๐ง Handling Errors #
- Prefer the generated
ModelSafeJsonParsing.fromJsonSafe(json)whenever you deserialize API data. - Catch
FormatExceptionat the repository/data-source layer and log or remap it once. - Fall back to manual helpers like
json.getSafeInt('field')when a model cannot be annotated. - Use
@SafeJsonConfigif you need global interception/logging without touching each call site.
try {
final summary = DoctorSummarySafeJsonParsing.fromJsonSafe(payload);
} on FormatException catch (error) {
logger.error('JSON mismatch: ${error.message}');
throw DataParseFailure.fromServer(error.message);
}
๐ช Riverpod integration #
If youโre using Riverpod, wrap the async work with AsyncValue.guard (or your own helper) and convert FormatException into an app-friendly error.
final doctorSummaryProvider = FutureProvider.autoDispose((ref) async {
final repository = ref.watch(summaryRepositoryProvider);
return AsyncValue.guard(() async {
final json = await repository.fetchSummaryJson();
return DoctorSummarySafeJsonParsing.fromJsonSafe(json);
}).when(
data: (value) => value,
error: (error, stack) {
if (error is FormatException) {
// Surface the enhanced message in your UI/logs
ref.read(loggerProvider).warn(error.message);
throw JsonParseFailure(error.message);
}
throw error;
},
loading: () => const AsyncLoading(),
);
});
Youโll get Riverpodโs normal AsyncError states while still benefiting from the detailed parsing message. If you want to render the detailed error in a widget (for example in a team list screen):
state.when(
loading: () => const CircularProgressIndicator(),
error: (error, _) {
final message = error is FormatException
? error.message
: (error.toString().isEmpty ? 'No data available!' : error.toString());
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
message,
textAlign: TextAlign.center,
),
),
);
},
data: (teamMembers) => ...,
);
๐ Complete Riverpod example #
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:json_annotation_tools/json_annotation_tools.dart';
part 'product.g.dart';
part 'product.safe_json_parsing.g.dart';
@JsonSerializable()
@SafeJsonParsing()
class Product {
Product({required this.id, required this.name, required this.price});
final int id;
final String name;
final double price;
factory Product.fromJson(Map<String, dynamic> json) =>
ProductSafeJsonParsing.fromJsonSafe(json);
}
final productRepositoryProvider = Provider<ProductRepository>((ref) {
return ProductRepository();
});
class ProductRepository {
Future<Map<String, dynamic>> fetchProductJson() async {
return {
'id': 1,
'name': 'Riverpod Coffee Mug',
'price': '19.99', // wrong type on purpose
};
}
}
final productProvider = FutureProvider.autoDispose<Product>((ref) async {
final repo = ref.watch(productRepositoryProvider);
return AsyncValue.guard(() async {
final json = await repo.fetchProductJson();
return Product.fromJson(json);
}).when(
data: (value) => value,
error: (error, stackTrace) {
if (error is FormatException) {
ref.read(loggerProvider).call(error.message);
rethrow;
}
throw error;
},
loading: () => throw StateError('unexpected loading state'),
);
});
final loggerProvider = Provider<void Function(String)>((ref) {
return (message) => print('LOG: $message');
});
void main() async {
final container = ProviderContainer();
try {
await container.read(productProvider.future);
} on FormatException catch (e) {
print('Enhanced error: ${e.message}');
}
}
Add flutter_riverpod to your pubspec (and run build_runner to generate the
part files) before running this snippet.
๐งช Testing & Debugging #
flutter test test/safe_json_parsing_test.dartโ regression test that asserts on the enhanced message.dart run tool/demo_safe_json_error.dartโ prints the full diagnostic for a controlled payload.rg "SafeJsonParsing" -g"*.g.dart"โ quick audit to check which models already have safe parsers.- Consider golden tests around high-value endpoints to lock in the detailed error copy.
๐ค Migration Guide #
From Standard JSON Serialization #
- Keep existing code working: Your current
fromJsonmethods continue to work - Add safe alternatives: Create
fromJsonSafemethods alongside existing ones - Test thoroughly: Use
fromJsonSafein tests to catch issues early - Gradual adoption: Migrate critical models first, then expand usage
๐ Performance Impact #
- Zero overhead during successful parsing
- Minimal impact during errors (only when you need the debugging info)
- Same memory usage as standard parsing
- Compatible with all existing JSON serialization patterns
๐ Platform Support #
This package works seamlessly across all Flutter/Dart platforms:
โ Fully Supported Platforms: #
- ๐ฑ iOS: Native iOS apps (iPhone/iPad)
- ๐ค Android: Native Android apps (phones/tablets)
- ๐ Web: Progressive Web Apps (PWA) and web browsers
- ๐ป macOS: Native desktop apps
- ๐ช Windows: Native desktop apps
- ๐ง Linux: Native desktop apps
๐ฆ Package Type: #
- Pure Dart package - no platform-specific code
- No native dependencies - works everywhere Flutter works
- Same API across all platforms
- Consistent behavior regardless of target platform
๐ฎ Try the Demo: #
Run our interactive example app to test on your preferred platform:
# iOS (requires Xcode and iOS Simulator/Device)
cd example_app && flutter run -d ios
# Android (requires Android SDK and Emulator/Device)
cd example_app && flutter run -d android
# Web (opens in default browser)
cd example_app && flutter run -d chrome
# macOS (requires macOS development setup)
cd example_app && flutter run -d macos
# Windows (requires Windows development setup)
cd example_app && flutter run -d windows
# Linux (requires Linux development setup)
cd example_app && flutter run -d linux
# Generate a static web build ready for hosting (Netlify/GitHub Pages/etc.)
cd example_app && flutter build web --release
๐ Troubleshooting #
Common Issues #
Q: "I'm getting errors about missing keys"
// Use getNullableSafe for optional fields
email: json.getNullableSafe('email', (v) => v as String),
Q: "How do I handle nested objects?"
// Parse nested objects recursively
address: json.getSafe('address', (v) => Address.fromJsonSafe(v as Map<String, dynamic>)),
Q: "Can I use this with existing json_serializable models?"
// Yes! Add safe methods alongside generated ones
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); // Generated
factory User.fromJsonSafe(Map<String, dynamic> json) { /* Your safe implementation */ }
๐ค Contributing #
We welcome contributions! Here's how to get involved:
๐ Report Issues: #
Found a bug or have a feature request?
- Create an issue: https://github.com/khokanuzzaman/json_annotation_tools/issues
- Include details: Error messages, code examples, expected vs actual behavior
๐ก Contribute Code: #
- Fork the repository: https://github.com/khokanuzzaman/json_annotation_tools
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
๐ Improve Documentation: #
- Fix typos or unclear explanations
- Add more examples or use cases
- Enhance visual documentation
๐งช Add Tests: #
- Write tests for new features
- Improve existing test coverage
- Add edge case testing
Please see our Contributing Guide for detailed guidelines.
๐ License #
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments #
- Built to complement the excellent
json_annotationandjson_serializablepackages - Inspired by the need for better JSON debugging in Flutter development
- Thanks to the Flutter community for feedback and suggestions
Made with โค๏ธ for the Flutter community
Stop debugging JSON parsing errors in the dark - see exactly what's wrong and fix it fast!