json_sentinel 0.1.0
json_sentinel: ^0.1.0 copied to clipboard
Lightweight runtime JSON key and type validation for Dart. No code generation required.
json_sentinel #
Lightweight runtime JSON key and type validation for Dart — no code generation required.
Validates a Map<String, dynamic> against an expected schema before you deserialise it,
catching malformed API responses early with a single, readable log entry per failure.
Features #
- Checks key existence and value types in one call
- Nullable fields, union types (
[int, double]), and optional fields all supported - Strict mode flags unexpected keys not declared in your schema
- Single log entry per call listing every problem as a bullet — nothing hidden by an early return
- Structured
json_previewandcontextpassed as extras for searchable Sentry/Crashlytics context - Configurable
escalateflag per call — control whether a failure is a breadcrumb or a capture - Returns
JsonValidationResultwithisValid+ programmaticerrorslist silence()for pure programmatic use with no log output- Optional
verbose: truefor debug-mode traces viadart:developer - Zero runtime dependencies — pure Dart, works in Flutter, server, and CLI
Getting started #
dependencies:
json_sentinel: ^0.1.0
Usage #
1. Wire in your logger once at startup #
JsonSentinel.configure(
(message, {error, stackTrace, extras, escalate}) {
// escalate: true → full capture; false → breadcrumb
if (escalate == true) {
Sentry.captureMessage(message, hint: Hint.withMap(extras ?? {}));
} else {
Sentry.addBreadcrumb(Breadcrumb(message: message));
}
},
verbose: true, // optional: dart:developer traces in debug mode
);
If configure() has not been called, failures fall back to dart:developer (visible in
DevTools Logging; suppressed in release builds). Always call configure() or silence() in
production code. Use JsonSentinel.logger to check whether a logger is already registered.
No logging needed?
Use silence() when you only want programmatic access to result.errors:
JsonSentinel.silence(); // installs a no-op logger — no output of any kind
silence() and configure() are mutually exclusive — call one or the other, never both.
2. Validate in tryFromJson #
static OrderResponse? tryFromJson(Map<String, dynamic> json) {
final result = JsonSentinel.validate(
json: json,
expectedTypes: {
'orderId': [int],
'depotCode': [String],
'litres': [int, double], // union — API may return either
'notes': [String, null], // nullable
},
optional: {'notes'}, // absent is fine; type-checked if present
context: 'OrderResponse',
escalate: true,
);
if (!result.isValid) return null;
return OrderResponse(
orderId: json['orderId'] as int,
depotCode: json['depotCode'] as String,
litres: (json['litres'] as num).toDouble(),
notes: json['notes'] as String?,
);
}
Nested JSON #
Validate the outer shape at each level; let child models validate their own fields.
Failures carry the child's own context string, so log entries are always pinpointed.
// Parent: validates top-level keys as List/Map only.
static PaginatedResponse? tryFromJson(Map<String, dynamic> json) {
final result = JsonSentinel.validate(
json: json,
expectedTypes: {
'data': [List],
'links': [Map],
'meta': [Map],
},
context: 'PaginatedResponse',
escalate: true,
);
if (!result.isValid) return null;
final meta = MetaModel.tryFromJson(
Map<String, dynamic>.from(json['meta'] as Map),
);
if (meta == null) return null;
// ...
}
// Child: validates its own schema, including nullable fields.
static MetaModel? tryFromJson(Map<String, dynamic> json) {
final result = JsonSentinel.validate(
json: json,
expectedTypes: {
'current_page': [int],
'from': [null, int], // null when page is empty
'last_page': [int],
'links': [List],
'path': [String],
'per_page': [int],
'to': [null, int], // null when page is empty
'total': [int],
},
context: 'MetaModel',
escalate: true,
);
if (!result.isValid) return null;
// ...
}
Optional fields #
Keys listed in optional are skipped when absent but still type-checked when present:
JsonSentinel.validate(
json: json,
expectedTypes: {'id': [int], 'nickname': [String]},
optional: {'nickname'},
);
Strict mode #
Reject keys present in the JSON but not declared in your schema:
JsonSentinel.validate(
json: json,
expectedTypes: {'id': [int]},
strict: true,
);
Programmatic error access #
final result = JsonSentinel.validate(json: json, expectedTypes: schema);
if (!result.isValid) {
for (final error in result.errors) {
print(error);
}
}
Example log output #
[OrderResponse] JSON validation failed (2 errors):
• Key 'orderId' has invalid type. Expected: int; Actual: String.
• Missing required key 'depotCode'.
Testing #
configure() and silence() both assert in debug mode if called twice. Call
JsonSentinel.resetLoggerForTesting() in tearDown to allow re-configuration across test cases:
setUp(() {
JsonSentinel.configure((message, {error, stackTrace, extras, escalate}) {
logs.add(message);
});
});
tearDown(JsonSentinel.resetLoggerForTesting);
Schema reference #
| Type list value | Meaning |
|---|---|
[int] |
Required, must be int |
[String] |
Required, must be String |
[bool] |
Required, must be bool |
[double] |
Required, must be double |
[num] |
Required, int or double |
[int, double] |
Required, int or double (explicit union) |
[Map] |
Required, any Map |
[List] |
Required, any List |
[String, null] |
Required, String or null |
[null, int] |
Required, null or int |
null or [] |
Required key, type not checked |
Add the key to optional to make presence itself optional.
Additional information #
- pub.dev package page
- File issues
- Changelog
- Contributions welcome — open an issue before starting significant work