Skip to content

Instantly share code, notes, and snippets.

@matanlurey
Created March 28, 2022 02:43
Show Gist options
  • Save matanlurey/c2deff6318e00b96b5a1d5af8e5c6aa6 to your computer and use it in GitHub Desktop.
Save matanlurey/c2deff6318e00b96b5a1d5af8e5c6aa6 to your computer and use it in GitHub Desktop.
A set of type-safe (Dart) JSON helpers that provide better error message handling.
/// A set of type-safe JSON helpers that provide better error message handling.
///
/// When used on well-structured JSON, these helpers have minimal overhead over
/// using `as` casts (i.e. `json['foo'] as String); however, when the cast fails
/// (which, should be rare in well-structured JSON) a format exception that
/// tries to highlight the offending source range is shown.
///
/// Additionally, the API is a bit less cumbersome than manual casting:
/// * [JsonArray]
/// * [JsonObject]
library json;
import 'dart:convert';
// ignore_for_file: cast_nullable_to_non_nullable
// When we do have a failure, the JSON should be easy to read.
const _prettyJson = JsonEncoder.withIndent(' ');
/// Extensions on `List<Object?>`, which could be a decoded JSON Array.
///
/// ```
/// // Standard method.
/// json.map((n) => SomethingElse.fromJson(n as Map<String, Object?>)).toList()
///
/// // Using this library.
/// json.readMappedObjects(SomethingeElse.fromJson)
/// ```
extension JsonArray on List<Object?> {
/// Returns a list of every element of the array, decoded from JSON.
List<T> readMappedObjects<T>(T Function(Map<String, Object?>) forEach) {
return map((element) {
if (element is Map<String, Object?>) {
return forEach(element);
}
throw FormatException(
'Expected nested JSON object, got ${element.runtimeType}',
);
}).toList();
}
}
/// Extensions on `Map<String, Object?>`, which could be a decoded JSON Object.
///
/// ```
/// // Standard method.
/// json['foo'] as Map<String, Object?>
///
/// // Using this library.
/// json.readObject('foo')
/// ```
extension JsonObject on Map<String, Object?> {
Never _throwFormatException(String key, Object? value, String expected) {
final source = _prettyJson.convert(this);
throw FormatException(
'"$key" expected $expected, got ${value.runtimeType}',
source,
source.indexOf(RegExp('\b"$key"\b:')),
);
}
T _readHelper<T>(String key) {
final value = this[key];
if (value is T) {
return value;
}
_throwFormatException(key, value, '$T');
}
/// Returns value of [key] as a sub-type of [num], otherwise throws.
T readNumber<T extends num>(String key) => _readHelper(key);
/// Returns value of [key] as a sub-type of [num] or `null`, otherwise throws.
T? readNumberOrNull<T extends num>(String key) => _readHelper(key);
/// Returns value of [key] as a [String], otherwise throws.
String readString(String key) => _readHelper(key);
/// Returns value of [key] as a [String] or `null`, otherwise throws.
String? readStringOrNull(String key) => _readHelper(key);
/// Returns value of [key] as a [JsonObject], otherwise throws.
Map<String, Object?> readObject(String key) => _readHelper(key);
/// Returns value of [key] as a [JsonObject] or `null`, otherwise throws.
Map<String, Object?>? readObjectOrNull(String key) => _readHelper(key);
/// Returns value of [key] as a [JsonArray], otherwise throws.
List<Object?> readArray(String key) => _readHelper(key);
/// Returns value of [key] as a [JsonArray], otherwise throws.
///
/// If the value was `null`, an empty array is returned instead.
List<Object?> readArrayOrEmpty(String key) => _readHelper(key) ?? [];
/// Returns value of [key] as a [JsonArray] or `null`, otherwise throws.
List<Object?>? readArrayOrNull(String key) => _readHelper(key);
}
@matanlurey
Copy link
Author

Here is an example of the errors you might get:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment