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.
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);
๐ฌ 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!
Libraries
- generator
- Code generator for @SafeJsonParsing annotation
- json_annotation_tools