From 0011b2d4c708e0ad10895c347c79de5db82f0eeb Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 1 Jun 2026 17:05:56 +0200 Subject: [PATCH] v1.4.8: Add recursive JSON-safe serialization utilities - Introduced extension methods for recursive serialization and deserialization of nested maps/lists: `toJsonValue()`, `fromJsonValue()`, `toJsonMap()`, `fromJsonMap()`, `toJsonList()`, and `fromJsonList()`. - Updated README.md to include new utilities. - Added tests for JSON value extensions. - Bumped version to 1.4.8. Signed-off-by: Hans Kokx --- CHANGELOG.md | 7 +++ README.md | 32 ++++++++++++++ lib/arcane_helper_utils.dart | 1 + lib/src/extensions/json_value.dart | 64 ++++++++++++++++++++++++++++ lib/src/extensions/jwt.dart | 2 +- pubspec.yaml | 2 +- test/extensions/json_value_test.dart | 62 +++++++++++++++++++++++++++ test/utils/json_converter_test.dart | 51 ++++++++++++++++++++++ 8 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 lib/src/extensions/json_value.dart create mode 100644 test/extensions/json_value_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b517f70..dea95c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.4.8 + +- Added extension methods for recursive JSON-safe serialization and deserialization of nested maps/lists: + - `toJsonValue()` / `fromJsonValue()` + - `toJsonMap()` / `fromJsonMap()` + - `toJsonList()` / `fromJsonList()` + ## 1.4.7 - Added the `isExpired` and `expiresSoon` getters to JWT tokens. diff --git a/README.md b/README.md index b7d9acf..3cc8ee0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ providing utility functions and extensions that simplify common tasks. - **Ticker Utility**: A utility class that facilitates time-based actions, perfect for animations or any timing-related operations. - **JSON Converter**: Simplifies the process of converting JSON data into Dart objects. +- **Recursive JSON Value Utilities**: Convert nested maps/lists into JSON-safe values and decode them back into typed nested structures. - **DateTime Extensions**: Adds additional functionality to the `DateTime` class, making it easier to format dates and calculate differences. - **String Extensions**: Enhances the `String` class by adding new methods for common transformations and checks. - **JWT Utilities**: Provides getters to parse a JWT token from a `String`, then get common properties from it. @@ -84,6 +85,37 @@ Here are some examples of how to use the utilities and extensions provided by th } ``` +### Recursive JSON Value Utilities + + Use these extension methods to recursively normalize nested data before + serializing it to JSON. + + ```dart + final payload = { + "metadata": { + "tags": ["arcane", "framework"], + }, + 123: DateTime.utc(2026, 1, 1), + }; + + final jsonSafe = payload.toJsonValue(); + // { + // "metadata": {"tags": ["arcane", "framework"]}, + // "123": "2026-01-01 00:00:00.000Z" + // } + + final decoded = jsonSafe.fromJsonValue(); + + // Extension methods: + final mapJson = payload.toJsonMap(); + final mapAgain = mapJson.fromJsonMap(); + + final nestedList = [ + [1, 2, {"deep": true}], + ]; + final nestedListJson = nestedList.toJsonList(); + ``` + ### DateTime Extensions These extensions add helpful methods to the `DateTime` class, making it easier to handle common date and time operations such as formatting, comparisons, and calculations. diff --git a/lib/arcane_helper_utils.dart b/lib/arcane_helper_utils.dart index 057d8eb..cc159f7 100644 --- a/lib/arcane_helper_utils.dart +++ b/lib/arcane_helper_utils.dart @@ -3,6 +3,7 @@ library arcane_helper_utils; export "package:arcane_helper_utils/src/classes/fixed_size_list.dart"; export "package:arcane_helper_utils/src/extensions/date_time.dart"; export "package:arcane_helper_utils/src/extensions/dynamic.dart"; +export "package:arcane_helper_utils/src/extensions/json_value.dart"; export "package:arcane_helper_utils/src/extensions/jwt.dart"; export "package:arcane_helper_utils/src/extensions/list.dart"; export "package:arcane_helper_utils/src/extensions/string.dart"; diff --git a/lib/src/extensions/json_value.dart b/lib/src/extensions/json_value.dart new file mode 100644 index 0000000..df72182 --- /dev/null +++ b/lib/src/extensions/json_value.dart @@ -0,0 +1,64 @@ +/// Extension methods for recursively serializing and deserializing JSON-like +/// values. +extension JsonValueObjectExtension on Object? { + /// Recursively serializes this value into a JSON-safe value. + Object? toJsonValue() => _JsonValueHelper._toJsonValue(this); + + /// Recursively deserializes this JSON-like value into nested Dart values. + Object? fromJsonValue() => _JsonValueHelper._fromJsonValue(this); + + /// Recursively deserializes this JSON-like value into a map. + /// + /// Returns `null` when this value is `null` or not a map. + Map? fromJsonMap() => _JsonValueHelper._fromJsonMap(this); +} + +/// Extension methods for JSON map serialization. +extension JsonValueMapExtension on Map { + /// Recursively serializes this map into a JSON-safe map with string keys. + Map toJsonMap() => _JsonValueHelper._toJsonMap(this); +} + +/// Extension methods for JSON list serialization and deserialization. +extension JsonValueListExtension on List { + /// Recursively serializes this list into a JSON-safe list. + List toJsonList() => _JsonValueHelper._toJsonList(this); + + /// Recursively deserializes this JSON-like list into nested Dart values. + List fromJsonList() => _JsonValueHelper._fromJsonList(this); +} + +abstract class _JsonValueHelper { + static Object? _toJsonValue(Object? value) { + if (value == null) return null; + if (value is String || value is num || value is bool) return value; + if (value is Map) return _toJsonMap(value); + if (value is List) return _toJsonList(value.cast()); + return value.toString(); + } + + static Map _toJsonMap(Map map) { + return map.map((k, v) => MapEntry(k.toString(), _toJsonValue(v))); + } + + static List _toJsonList(List list) { + return list.map(_toJsonValue).toList(); + } + + static Object? _fromJsonValue(Object? value) { + if (value == null) return null; + if (value is Map) return _fromJsonMap(value); + if (value is List) return _fromJsonList(value.cast()); + return value; + } + + static Map? _fromJsonMap(Object? value) { + if (value == null) return null; + if (value is! Map) return null; + return value.map((k, v) => MapEntry(k.toString(), _fromJsonValue(v))); + } + + static List _fromJsonList(List list) { + return list.map(_fromJsonValue).toList(); + } +} diff --git a/lib/src/extensions/jwt.dart b/lib/src/extensions/jwt.dart index c77f298..0e89438 100644 --- a/lib/src/extensions/jwt.dart +++ b/lib/src/extensions/jwt.dart @@ -1,6 +1,6 @@ import "dart:convert"; -typedef JwtPayload = Map; +typedef JwtPayload = Map; /// An extension on `String` to extract useful information from JSON Web Tokens (JWT). /// diff --git a/pubspec.yaml b/pubspec.yaml index 95b832d..635ab5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: arcane_helper_utils description: Provides a variety of helpful utilities and extensions for Flutter and Dart. -version: 1.4.7 +version: 1.4.8 repository: https://github.com/hanskokx/arcane_helper_utils issue_tracker: https://github.com/hanskokx/arcane_helper_utils/issues diff --git a/test/extensions/json_value_test.dart b/test/extensions/json_value_test.dart new file mode 100644 index 0000000..eff3eef --- /dev/null +++ b/test/extensions/json_value_test.dart @@ -0,0 +1,62 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + group("JsonValue extensions", () { + test("toJsonList serializes nested list values", () { + final List nested = [ + [ + 1, + 2, + {"ok": true}, + ], + ]; + + final result = nested.toJsonList(); + + expect(result, isA>()); + expect((result.first as List)[0], 1); + expect( + ((result.first as List)[2] as Map)["ok"], + isTrue, + ); + }); + + test("toJsonMap serializes map keys and nested values", () { + final map = { + 123: "value", + "nested": {"a": 1}, + }; + + expect(map.toJsonMap().containsKey("123"), isTrue); + expect( + (map.toJsonMap()["nested"] as Map)["a"], + 1, + ); + }); + + test("fromJsonMap decodes nested structures from Object?", () { + final Object value = { + "meta": { + "list": [ + {"x": 1}, + ], + }, + }; + + final decoded = value.fromJsonMap(); + final meta = decoded!["meta"]! as Map; + final list = meta["list"]! as List; + + expect((list.first as Map)["x"], 1); + }); + + test("toJsonValue and fromJsonValue round-trip primitives", () { + const Object original = 42; + final jsonValue = original.toJsonValue(); + final decoded = jsonValue.fromJsonValue(); + + expect(decoded, 42); + }); + }); +} diff --git a/test/utils/json_converter_test.dart b/test/utils/json_converter_test.dart index 4317700..402a4f6 100644 --- a/test/utils/json_converter_test.dart +++ b/test/utils/json_converter_test.dart @@ -48,4 +48,55 @@ void main() { expect(converter.toJson(null), null); }); }); + + group("Json value extensions", () { + test("toJsonValue recursively encodes nested maps and lists", () { + final input = { + "user": { + "name": "Hans", + "stats": [1, 2, true], + }, + 42: [ + {"nested": "value"}, + ], + }; + + final result = input.toJsonValue() as Map; + + expect(result["42"], isA>()); + expect(result["user"], isA>()); + final user = result["user"]! as Map; + expect(user["name"], "Hans"); + expect(user["stats"], [1, 2, true]); + }); + + test("toJsonValue stringifies unsupported leaves", () { + final now = DateTime.utc(2026, 1, 1); + + expect(now.toJsonValue(), now.toString()); + }); + + test("fromJsonValue recursively decodes nested maps and lists", () { + final input = { + "metadata": { + "list": [ + {"ok": true}, + 7, + ], + }, + }; + + final result = input.fromJsonValue() as Map; + final metadata = result["metadata"]! as Map; + final list = metadata["list"]! as List; + + expect((list.first as Map)["ok"], isTrue); + expect(list[1], 7); + }); + + test("fromJsonMap returns null when value is not a map", () { + expect(("not a map" as Object?).fromJsonMap(), isNull); + expect((null as Object?).fromJsonMap(), isNull); + }); + }); }