rj_safe_parser 0.4.0
rj_safe_parser: ^0.4.0 copied to clipboard
RJ Safe Parser: Annotation-based Dart generator for JSON parsing, mapping, and validation with zero boilerplate.
rj_safe_parser #
One annotation on the class. Zero per-field boilerplate.
rj_safe_parser is a Dart code-generation package inspired by Spring Boot's @Entity.
Place @RjSafeParsable() once on a class, run build_runner, and get a fully
type-safe fromMap() / toMap() with smart coercion for free — no per-field
annotations, no manual schema, no handwritten converters.
Why rj_safe_parser? #
APIs in the real world rarely send perfectly typed JSON.
You get "7" instead of 7, 1 instead of true, a Unix timestamp instead
of an ISO string. Every other parser either crashes or forces you to write
field-by-field conversion code.
rj_safe_parser reads your existing Dart field types and generates all the
boilerplate automatically — including smart coercion that handles messy
real-world data without any extra configuration.
// Dirty API response — wrong types everywhere
final json = {
'id': '7', // String → coerced to int
'score': '9.8', // String → coerced to double
'isActive': 1, // int → coerced to bool
'createdAt': 1712620800, // int → coerced to DateTime
'address': {'city': 'Dhaka', 'zip': '1212'}, // zip String → int
};
// One line. Just works.
final user = RjUserModel.fromMap(json);
Features #
- ✅ One annotation — no
@JsonKey,@HiveField, or per-field decoration needed - ✅ Smart coercion —
'7'→int,1→bool, Unix timestamp →DateTime - ✅
@RjKey— map JSON keys to Dart fields (e.g. snake_case JSON → camelCase Dart) - ✅ Nested models — auto-detected when also annotated with
@RjSafeParsable() - ✅ Lists —
List<String>,List<MyModel>— all handled automatically - ✅ Map<String, V> —
Map<String, int>,Map<String, bool>— values coerced automatically - ✅ Enums — by-name or by-index (
@RjEnum(byIndex: true)) - ✅ Nullable fields —
String?= optional field, absent key safely yieldsnull - ✅ Strict mode — optionally reject unknown keys for hard API validation
- ✅ Dot-path errors — exceptions include the full field path (e.g.
address.zip) - ✅ Round-trip safe —
toMap()serialises back to the original shape - ✅ Pure Dart — no
dart:mirrors, no Flutter dependency, fully tree-shakeable
vs. json_serializable #
json_serializable |
rj_safe_parser |
|
|---|---|---|
| Annotations per model | 1 class + N fields | 1 class only |
Wrong types (e.g. "7" for int) |
❌ throws | ✅ coerced automatically |
| Unix timestamp → DateTime | manual converter | ✅ built-in |
1 / 0 → bool |
manual converter | ✅ built-in |
| Enums | @JsonKey(unknownEnumValue:…) |
@RjEnum() or @RjEnum(byIndex:true) |
Map<String, V> |
@JsonKey(fromJson:…) |
✅ automatic value coercion |
| JSON key mapping | @JsonKey(name: ...) |
@RjKey(...) or .snakeCase |
| Add/rename a field | update class + annotation | update class only |
| Nested models | @JsonSerializable() on each |
@RjSafeParsable() on each |
| Strict unknown-key mode | ❌ | ✅ strict: true |
Installation #
Add to your pubspec.yaml:
dependencies:
rj_safe_parser: ^0.4.0
dev_dependencies:
build_runner: ^2.4.0
Then run:
dart pub get
Quick start #
1. Annotate your class #
import 'package:rj_safe_parser/rj_safe_parser.dart';
part 'rj_user_model.g.dart'; // ← generated by build_runner
@RjSafeParsable()
class RjUserModel {
final int id;
final String name;
final String? nickname; // nullable → absent key returns null
final double score;
final bool isActive;
final DateTime createdAt;
final Uri profileUrl;
final RjAddressModel address; // nested @RjSafeParsable class
final List<String> tags;
final List<RjAddressModel> history;
const RjUserModel({
required this.id,
required this.name,
this.nickname,
required this.score,
required this.isActive,
required this.createdAt,
required this.profileUrl,
required this.address,
required this.tags,
required this.history,
});
factory RjUserModel.fromMap(Map<String, dynamic> map) =>
_$RjUserModelFromMap(map);
Map<String, dynamic> toMap() => _$RjUserModelToMap(this);
}
2. Annotate nested models the same way #
import 'package:rj_safe_parser/rj_safe_parser.dart';
part 'rj_address_model.g.dart';
@RjSafeParsable()
class RjAddressModel {
final String city;
final int zip;
const RjAddressModel({required this.city, required this.zip});
factory RjAddressModel.fromMap(Map<String, dynamic> map) =>
_$RjAddressModelFromMap(map);
Map<String, dynamic> toMap() => _$RjAddressModelToMap(this);
}
3. Run the generator #
dart run build_runner build
For continuous generation during development:
dart run build_runner watch
If you see conflicts from a previous run:
dart run build_runner build --delete-conflicting-outputs
4. Use it #
final user = RjUserModel.fromMap(dirtyJson);
print(user.id); // 7 (int, coerced from '7')
print(user.isActive); // true (bool, coerced from 1)
print(user.createdAt); // 2024-04-09 ... (DateTime from Unix ts)
print(user.address.city); // Dhaka
// Round-trip back to map
final map = user.toMap();
Supported type coercions #
| Dart type | Accepted raw values |
|---|---|
int |
int, double (truncated), bool (0/1), numeric String |
double |
double, int, numeric String |
bool |
bool, int (0/1), String (true / false / yes / no / 1 / 0) |
String |
any value — .toString() is called |
DateTime |
DateTime, Unix int/double (seconds or milliseconds), ISO-8601 String |
Uri |
Uri, any String |
Enum |
.name String (default), or int index when @RjEnum(byIndex: true) |
Map<String, V> |
Map — each value coerced to V; missing key throws unless nullable |
List<T> |
List — each element is coerced to T; missing key throws unless List<T>? |
NestedModel |
Map<String, dynamic> — when annotated with @RjSafeParsable() |
Note: Required (non-nullable)
Listfields now throwRjParseExceptionwhen the key is missing. Previously they silently returned[]. UseList<T>?if you want to accept a missing key gracefully.
Annotation options #
@RjSafeParsable(
strict: true, // Reject unknown keys — throws RjParseException (default: false)
)
class MyModel { ... }
strict: false (default) — unknown keys in the source map are ignored with a warning
in RjParseResult.warnings. Useful for APIs that add fields over time.
strict: true — unknown keys throw RjParseException. Useful when you want
to enforce an exact contract.
@RjKey — JSON key mapping #
When the JSON key differs from the Dart field name, use @RjKey on the field:
@RjSafeParsable()
class Photo {
final String id;
final String author;
@RjKey('download_url')
final String downloadUrl;
Photo({required this.id, required this.author, required this.downloadUrl});
factory Photo.fromMap(Map<String, dynamic> map) => _$PhotoFromMap(map);
Map<String, dynamic> toMap() => _$PhotoToMap(this);
}
The generated code reads from 'download_url' in JSON, stores it in downloadUrl,
and serialises back to 'download_url' in toMap().
Automatic snake_case conversion #
Instead of specifying the key manually, use RjKey.snakeCase() for automatic
conversion (downloadUrl → download_url):
@RjKey.snakeCase()
final String downloadUrl;
@RjEnum — enum serialisation #
Dart enum fields are supported out of the box. By default the JSON value is
matched against the enum constant's .name string:
enum Status { active, inactive, pending }
@RjSafeParsable()
class Order {
@RjEnum() // 'active' → Status.active
final Status status;
}
Use @RjEnum(byIndex: true) to match by ordinal position instead:
enum Priority { low, medium, high }
@RjSafeParsable()
class Task {
@RjEnum(byIndex: true) // 0 → Priority.low, 2 → Priority.high
final Priority priority;
}
Nullable enum fields (Status?) work as expected — absent key or explicit
null yields null. toMap() serialises back to .name (default) or
.index (when byIndex: true).
If the annotation is omitted from an enum field the generator defaults to
by-name matching — @RjEnum() is optional when byIndex: false.
Error handling #
All parse errors throw RjParseException with a dot-notation field path:
try {
final user = RjUserModel.fromMap(badJson);
} on RjParseException catch (e) {
print(e); // RjParseException at "address.zip": Cannot coerce "abc" → int
}
License #
MIT — see LICENSE