JSON Helper For Clean Architecture Flutter Models
Groovy way to encode and decode JSON entities without code generation
One of the use cases of model use in flutter apps using clean architecture is the decoding and encoding of JSON objects to interact with online API entry points. While some packages use build runner and code generation; there is a better approach that can get rid of using code generation.
Not only that, one can provide some casting syntax sugar to avoid having to do casts to type multiple times in a model. Onward to some JSON encode and decode magic.
JSON Solution Without Code Generation
This solution comes from the Dart Http server put out by Conduit. The original code is open source under BSD-claus 2 license, so I just soft forked it.
The main reasoning for avoiding code generation is that with large groups of models it takes some time to generate the code. The other reason for adapting to Conduit's JSON approach to encoding and decoding is that Conduit has added some syntax sugar so we are no longer forced into casting types when encoding and decoding JSON entities.
To start out we need a base class for the encoding or decoding object:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// | |
// Modified from original by codable BSD 2 clause | |
// license 2019. | |
import 'package:flutter/foundation.dart'; | |
import 'package:json_helpers/json_helpers/cast.dart' as cast; | |
import 'package:json_helpers/json_helpers/keyed_archive.dart'; | |
/// A base class for encodable and decodable objects. | |
/// | |
/// Types that can read or write their values to a document should extend this abstract class. | |
/// By overriding [decode] and [encode], an instance of this type will read or write its values | |
/// into a data container that can be transferred into formats like JSON or YAML. | |
abstract class Coding { | |
Uri? referenceURI; | |
Map<String, cast.Cast<dynamic>>? get castMap => null; | |
@mustCallSuper | |
void decode(KeyedArchive object) { | |
referenceURI = object.referenceURI; | |
object.castValues(castMap); | |
} | |
// would prefer to write referenceURI to object here, but see note in KeyedArchive._encodedObject | |
void encode(KeyedArchive object); | |
} |
With the major lifting ocuring in the KeyedArchive class:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// | |
// Modified from original by codable BSD 2 clause | |
// license 2019. | |
import 'dart:collection'; | |
import 'package:json_helpers/json_helpers/cast.dart' as cast; | |
import 'package:json_helpers/json_helpers/codable.dart'; | |
import 'package:json_helpers/json_helpers/coding.dart'; | |
import 'package:json_helpers/json_helpers/list.dart'; | |
import 'package:json_helpers/json_helpers/resolver.dart'; | |
/// A container for a dynamic data object that can be decoded into [Coding] objects. | |
/// | |
/// A [KeyedArchive] is a [Map], but it provides additional behavior for decoding [Coding] objects | |
/// and managing JSON Schema references ($ref) through methods like [decode], [decodeObject], etc. | |
/// | |
/// You create a [KeyedArchive] by invoking [KeyedArchive.unarchive] and passing data decoded from a | |
/// serialization format like JSON and YAML. A [KeyedArchive] is then provided as an argument to | |
/// a [Coding] subclass' [Coding.decode] method. | |
/// | |
/// final json = json.decode(...); | |
/// final archive = KeyedArchive.unarchive(json); | |
/// final person = Person()..decode(archive); | |
/// | |
/// You may also create [KeyedArchive]s from [Coding] objects so that they can be serialized. | |
/// | |
/// final person = Person()..name = "Bob"; | |
/// final archive = KeyedArchive.archive(person); | |
/// final json = json.encode(archive); | |
/// | |
class KeyedArchive extends Object with MapMixin<String, dynamic> implements Referencable { | |
/// Use [unarchive] instead. | |
KeyedArchive(this._map) { | |
_recode(); | |
} | |
/// Unarchives [data] into a [KeyedArchive] that can be used by [Coding.decode] to deserialize objects. | |
/// | |
/// Each [Map] in [data] (including [data] itself) is converted to a [KeyedArchive]. | |
/// Each [List] in [data] is converted to a [ListArchive]. These conversions occur for deeply nested maps | |
/// and lists. | |
/// | |
/// If [allowReferences] is true, JSON Schema references will be traversed and decoded objects | |
/// will contain values from the referenced object. This flag defaults to false. | |
KeyedArchive.unarchive(this._map, {bool allowReferences = false}) { | |
_recode(); | |
if (allowReferences) { | |
resolveOrThrow(ReferenceResolver(this)); | |
} | |
} | |
/// Archives a [Coding] object into a [Map] that can be serialized into format like JSON or YAML. | |
/// | |
/// Note that the return value of this method, as well as all other [Map] and [List] objects | |
/// embedded in the return value, are instances of [KeyedArchive] and [ListArchive]. These types | |
/// implement [Map] and [List], respectively. | |
/// | |
/// If [allowReferences] is true, JSON Schema references in the emitted document will be validated. | |
/// Defaults to false. | |
static Map<String, dynamic> archive(Coding root, {bool allowReferences = false}) { | |
final archive = KeyedArchive({}); | |
root.encode(archive); | |
if (allowReferences) { | |
archive.resolveOrThrow(ReferenceResolver(archive)); | |
} | |
return archive.toPrimitive(); | |
} | |
KeyedArchive._empty() : _map = <String, dynamic>{}; | |
/// A reference to another object in the same document. | |
/// | |
/// This value is a path-only [Uri]. Each path segment is a key, starting | |
/// at the document root this object belongs to. For example, the path '/components/all' | |
/// would reference the object as returned by `document['components']['all']`. | |
/// | |
/// Assign values to this property using the default [Uri] constructor and its path argument. | |
/// This property is serialized as a [Uri] fragment, e.g. `#/components/all`. | |
/// | |
/// Example: | |
/// | |
/// final object = new MyObject() | |
/// ..referenceURI = Uri(path: "/other/object"); | |
/// archive.encodeObject("object", object); | |
/// | |
Uri? referenceURI; | |
Map<String, dynamic> _map; | |
Coding? _inflated; | |
KeyedArchive? _objectReference; | |
/// Typecast the values in this archive. | |
/// | |
/// Prefer to override [Coding.castMap] instead of using this method directly. | |
/// | |
/// This method will recursively type values in this archive to the desired type | |
/// for a given key. Use this method (or [Coding.castMap]) for decoding `List` and `Map` | |
/// types, where the values are not `Coding` objects. | |
/// | |
/// You must `import 'package:codable/cast.dart' as cast;`. | |
/// | |
/// Usage: | |
/// | |
/// final dynamicObject = { | |
/// "key": <dynamic>["foo", "bar"] | |
/// }; | |
/// final archive = KeyedArchive.unarchive(dynamicObject); | |
/// archive.castValues({ | |
/// "key": cast.List(cast.String) | |
/// }); | |
/// | |
/// // This now becomes a valid assignment | |
/// List<String> key = archive.decode("key"); | |
/// | |
void castValues(Map<String, cast.Cast>? schema) { | |
if (schema == null) { | |
return; | |
} | |
final caster = cast.Keyed(schema); | |
_map = caster.cast(_map); | |
if (_objectReference != null) { | |
// todo: can optimize this by only running it once | |
_objectReference!._map = caster.cast(_objectReference!._map); | |
} | |
} | |
/// store the [key]/[value] pair into the map | |
@override | |
void operator []=(covariant String key, dynamic value) { | |
_map[key] = value; | |
} | |
@override | |
dynamic operator [](covariant Object key) => _getValue(key as String); | |
@override | |
Iterable<String> get keys => _map.keys; | |
@override | |
void clear() => _map.clear(); | |
@override | |
dynamic remove(Object? key) => _map.remove(key); | |
Map<String, dynamic> toPrimitive() { | |
final out = <String, dynamic>{}; | |
_map.forEach((key, val) { | |
if (val is KeyedArchive) { | |
out[key] = val.toPrimitive(); | |
} else if (val is ListArchive) { | |
out[key] = val.toPrimitive(); | |
} else { | |
out[key] = val; | |
} | |
}); | |
return out; | |
} | |
dynamic _getValue(String? key) { | |
if (_map.containsKey(key)) { | |
return _map[key]; | |
} | |
return _objectReference?._getValue(key); | |
} | |
void _recode() { | |
const caster = cast.Map(cast.string, cast.any); | |
final keys = _map.keys.toList(); | |
for (final key in keys) { | |
final val = _map[key]; | |
if (val is Map) { | |
_map[key] = KeyedArchive(caster.cast(val)); | |
} else if (val is List) { | |
_map[key] = ListArchive.from(val); | |
} else if (key == r"$ref") { | |
referenceURI = Uri.parse(Uri.parse(val.toString()).fragment); | |
} | |
} | |
} | |
/// Validates [referenceURI]s for this object and any objects it contains. | |
/// | |
/// This method is automatically invoked by both [KeyedArchive.unarchive] and [KeyedArchive.archive]. | |
@override | |
void resolveOrThrow(ReferenceResolver coder) { | |
if (referenceURI != null) { | |
_objectReference = coder.resolve(referenceURI!); | |
if (_objectReference == null) { | |
throw ArgumentError("Invalid document. Reference '#${referenceURI!.path}' does not exist in document."); | |
} | |
} | |
_map.forEach((key, val) { | |
if (val is KeyedArchive) { | |
val.resolveOrThrow(coder); | |
} else if (val is ListArchive) { | |
val.resolveOrThrow(coder); | |
} | |
}); | |
} | |
/* decode */ | |
T? _decodedObject<T extends Coding?>(KeyedArchive? raw, T Function() inflate) { | |
if (raw == null) { | |
return null; | |
} | |
if (raw._inflated == null) { | |
raw._inflated = inflate(); | |
raw._inflated!.decode(raw); | |
} | |
return raw._inflated as T?; | |
} | |
/// Returns the object associated by [key]. | |
/// | |
/// If [T] is inferred to be a [Uri] or [DateTime], | |
/// the associated object is assumed to be a [String] and an appropriate value is parsed | |
/// from that string. | |
/// | |
/// If this object is a reference to another object (via [referenceURI]), this object's key-value | |
/// pairs will be searched first. If [key] is not found, the referenced object's key-values pairs are searched. | |
/// If no match is found, null is returned. | |
T? decode<T>(String key) { | |
final v = _getValue(key); | |
if (v == null) { | |
return null; | |
} | |
if (T == Uri) { | |
return Uri.parse(v.toString()) as T; | |
} else if (T == DateTime) { | |
return DateTime.parse(v.toString()) as T; | |
} | |
return v as T?; | |
} | |
/// Returns the instance of [T] associated with [key]. | |
/// | |
/// [inflate] must create an empty instance of [T]. The value associated with [key] | |
/// must be a [KeyedArchive] (a [Map]). The values of the associated object are read into | |
/// the empty instance of [T]. | |
T? decodeObject<T extends Coding>(String key, T Function() inflate) { | |
final val = _getValue(key); | |
if (val == null) { | |
return null; | |
} | |
if (val is! KeyedArchive) { | |
throw ArgumentError("Cannot decode key '$key' into '$T', because the value is not a Map. Actual value: '$val'."); | |
} | |
return _decodedObject(val, inflate); | |
} | |
/// Returns a list of [T]s associated with [key]. | |
/// | |
/// [inflate] must create an empty instance of [T]. The value associated with [key] | |
/// must be a [ListArchive] (a [List] of [Map]). For each element of the archived list, | |
/// [inflate] is invoked and each object in the archived list is decoded into | |
/// the instance of [T]. | |
List<T?>? decodeObjects<T extends Coding>(String key, T? Function() inflate) { | |
final val = _getValue(key); | |
if (val == null) { | |
return null; | |
} | |
if (val is! List) { | |
throw ArgumentError("Cannot decode key '$key' as 'List<$T>', because value is not a List. Actual value: '$val'."); | |
} | |
return val.map((v) => _decodedObject(v as KeyedArchive?, inflate)).toList().cast<T?>(); | |
} | |
/// Returns a map of [T]s associated with [key]. | |
/// | |
/// [inflate] must create an empty instance of [T]. The value associated with [key] | |
/// must be a [KeyedArchive] (a [Map]), where each value is a [T]. | |
/// For each key-value pair of the archived map, [inflate] is invoked and | |
/// each value is decoded into the instance of [T]. | |
Map<String, T?>? decodeObjectMap<T extends Coding>(String key, T Function() inflate) { | |
final v = _getValue(key); | |
if (v == null) { | |
return null; | |
} | |
if (v is! Map<String, dynamic>) { | |
throw ArgumentError( | |
"Cannot decode key '$key' as 'Map<String, $T>', because value is not a Map. Actual value: '$v'."); | |
} | |
return {for (var k in v.keys) k: _decodedObject(v[k] as KeyedArchive?, inflate)}; | |
} | |
/* encode */ | |
Map<String?, dynamic>? _encodedObject(Coding? object) { | |
if (object == null) { | |
return null; | |
} | |
// todo: an object can override the values it inherits from its | |
// reference object. These values are siblings to the $ref key. | |
// they are currently not being emitted. the solution is probably tricky. | |
// letting encode run as normal would stack overflow when there is a cyclic | |
// reference between this object and another. | |
final json = KeyedArchive._empty() | |
.._map = {} | |
..referenceURI = object.referenceURI; | |
if (json.referenceURI != null) { | |
json._map[r"$ref"] = Uri(fragment: json.referenceURI!.path).toString(); | |
} else { | |
object.encode(json); | |
} | |
return json; | |
} | |
/// Encodes [value] into this object for [key]. | |
/// | |
/// If [value] is a [DateTime], it is first encoded as an ISO 8601 string. | |
/// If [value] is a [Uri], it is first encoded to a string. | |
/// | |
/// If [value] is null, no value is encoded and the [key] will not be present | |
/// in the resulting archive. | |
void encode(String key, dynamic value) { | |
if (value == null) { | |
return; | |
} | |
if (value is DateTime) { | |
_map[key] = value.toIso8601String(); | |
} else if (value is Uri) { | |
_map[key] = value.toString(); | |
} else { | |
_map[key] = value; | |
} | |
} | |
/// Encodes a [Coding] object into this object for [key]. | |
/// | |
/// This invokes [Coding.encode] on [value] and adds the object | |
/// to this archive for the key [key]. | |
void encodeObject(String key, Coding? value) { | |
if (value == null) { | |
return; | |
} | |
_map[key] = _encodedObject(value); | |
} | |
/// Encodes list of [Coding] objects into this object for [key]. | |
/// | |
/// This invokes [Coding.encode] on each object in [value] and adds the list of objects | |
/// to this archive for the key [key]. | |
void encodeObjects(String key, List<Coding?>? value) { | |
if (value == null) { | |
return; | |
} | |
_map[key] = ListArchive.from(value.map((v) => _encodedObject(v)).toList()); | |
} | |
/// Encodes map of [Coding] objects into this object for [key]. | |
/// | |
/// This invokes [Coding.encode] on each value in [value] and adds the map of objects | |
/// to this archive for the key [key]. | |
void encodeObjectMap<T extends Coding?>(String key, Map<String, T>? value) { | |
if (value == null) { | |
return; | |
} | |
final object = KeyedArchive({}); | |
value.forEach((k, v) { | |
object[k] = _encodedObject(v); | |
}); | |
_map[key] = object; | |
} | |
} |
And we have the type cast syntax sugar:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// | |
// Modified from original by codable BSD 2 clause | |
// license 2019. | |
import 'dart:async' as async; | |
import 'dart:core' as core; | |
import 'dart:core' hide Map, String, int; | |
class FailedCast implements core.Exception { | |
dynamic context; | |
dynamic key; | |
core.String message; | |
FailedCast(this.context, this.key, this.message); | |
@override | |
core.String toString() { | |
if (key == null) { | |
return "Failed cast at $context: $message"; | |
} | |
return "Failed cast at $context $key: $message"; | |
} | |
} | |
abstract class Cast<T> { | |
const Cast(); | |
T _cast(dynamic from, core.String context, dynamic key); | |
T cast(dynamic from) => _cast(from, "toplevel", null); | |
} | |
class AnyCast extends Cast<dynamic> { | |
const AnyCast(); | |
@override | |
dynamic _cast(dynamic from, core.String context, dynamic key) => from; | |
} | |
class IntCast extends Cast<core.int> { | |
const IntCast(); | |
@override | |
core.int _cast(dynamic from, core.String context, dynamic key) => | |
from is core.int ? from : throw FailedCast(context, key, "$from is not an int"); | |
} | |
class DoubleCast extends Cast<core.double> { | |
const DoubleCast(); | |
@override | |
core.double _cast(dynamic from, core.String context, dynamic key) => | |
from is core.double ? from : throw FailedCast(context, key, "$from is not an double"); | |
} | |
class StringCast extends Cast<core.String> { | |
const StringCast(); | |
@override | |
core.String _cast(dynamic from, core.String context, dynamic key) => | |
from is core.String ? from : throw FailedCast(context, key, "$from is not a String"); | |
} | |
class BoolCast extends Cast<core.bool> { | |
const BoolCast(); | |
@override | |
core.bool _cast(dynamic from, core.String context, dynamic key) => | |
from is core.bool ? from : throw FailedCast(context, key, "$from is not a bool"); | |
} | |
class Map<K, V> extends Cast<core.Map<K, V>> { | |
final Cast<K> _key; | |
final Cast<V> _value; | |
const Map(Cast<K> key, Cast<V> value) | |
: _key = key, | |
_value = value; | |
@override | |
core.Map<K, V> _cast(dynamic from, core.String context, dynamic key) { | |
if (from is core.Map) { | |
final result = <K, V>{}; | |
for (final key in from.keys) { | |
final newKey = _key._cast(key, "map entry", key); | |
result[newKey] = _value._cast(from[key], "map entry", key); | |
} | |
return result; | |
} | |
return throw FailedCast(context, key, "not a map"); | |
} | |
} | |
class StringMap<V> extends Cast<core.Map<core.String, V>> { | |
final Cast<V> _value; | |
const StringMap(Cast<V> value) : _value = value; | |
@override | |
core.Map<core.String, V> _cast(dynamic from, core.String context, dynamic key) { | |
if (from is core.Map) { | |
final result = <core.String, V>{}; | |
for (final core.String key in from.keys as core.Iterable<core.String>) { | |
result[key] = _value._cast(from[key], "map entry", key); | |
} | |
return result; | |
} | |
return throw FailedCast(context, key, "not a map"); | |
} | |
} | |
class List<E> extends Cast<core.List<E?>> { | |
final Cast<E> _entry; | |
const List(Cast<E> entry) : _entry = entry; | |
@override | |
core.List<E?> _cast(dynamic from, core.String context, dynamic key) { | |
if (from is core.List) { | |
final length = from.length; | |
final result = core.List<E?>.filled(length, null); | |
for (core.int i = 0; i < length; ++i) { | |
if (from[i] != null) { | |
result[i] = _entry._cast(from[i], "list entry", i); | |
} else { | |
result[i] = null; | |
} | |
} | |
return result; | |
} | |
return throw FailedCast(context, key, "not a list"); | |
} | |
} | |
class Keyed<K, V> extends Cast<core.Map<K, V?>> { | |
Iterable<K> get keys => _map.keys; | |
final core.Map<K, Cast<V>> _map; | |
const Keyed(core.Map<K, Cast<V>> map) : _map = map; | |
@override | |
core.Map<K, V?> _cast(dynamic from, core.String context, dynamic key) { | |
final core.Map<K, V?> result = {}; | |
if (from is core.Map) { | |
for (final K key in from.keys as core.Iterable<K>) { | |
if (_map.containsKey(key)) { | |
result[key] = _map[key]!._cast(from[key], "map entry", key); | |
} else { | |
result[key] = from[key] as V?; | |
} | |
} | |
return result; | |
} | |
return throw FailedCast(context, key, "not a map"); | |
} | |
} | |
class OneOf<S, T> extends Cast<dynamic> { | |
final Cast<S> _left; | |
final Cast<T> _right; | |
const OneOf(Cast<S> left, Cast<T> right) | |
: _left = left, | |
_right = right; | |
@override | |
dynamic _cast(dynamic from, core.String context, dynamic key) { | |
try { | |
return _left._cast(from, context, key); | |
} on FailedCast { | |
return _right._cast(from, context, key); | |
} | |
} | |
} | |
class Apply<S, T> extends Cast<T> { | |
final Cast<S> _first; | |
final T Function(S) _transform; | |
const Apply(T Function(S) transform, Cast<S> first) | |
: _transform = transform, | |
_first = first; | |
@override | |
T _cast(dynamic from, core.String context, dynamic key) => _transform(_first._cast(from, context, key)); | |
} | |
class Future<E> extends Cast<async.Future<E>> { | |
final Cast<E> _value; | |
const Future(Cast<E> value) : _value = value; | |
@override | |
async.Future<E> _cast(dynamic from, core.String context, dynamic key) { | |
if (from is async.Future) { | |
return from.then(_value.cast); | |
} | |
return throw FailedCast(context, key, "not a Future"); | |
} | |
} | |
const any = AnyCast(); | |
const bool = BoolCast(); | |
const int = IntCast(); | |
const double = DoubleCast(); | |
const string = StringCast(); |
Codable:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// | |
// Modified from original by codable BSD 2 clause | |
// license 2019. | |
import 'package:json_helpers/json_helpers/resolver.dart'; | |
abstract class Referencable { | |
void resolveOrThrow(ReferenceResolver resolver); | |
} |
List:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// | |
// Modified from original by codable BSD 2 clause | |
// license 2019. | |
import 'dart:collection'; | |
import 'package:json_helpers/json_helpers/codable.dart'; | |
import 'package:json_helpers/json_helpers/keyed_archive.dart'; | |
import 'package:json_helpers/json_helpers/resolver.dart'; | |
/// A list of values in a [KeyedArchive]. | |
/// | |
/// This object is a [List] that has additional behavior for encoding and decoding [Coding] objects. | |
class ListArchive extends Object with ListMixin<dynamic> implements Referencable { | |
final List<dynamic> _inner; | |
ListArchive() : _inner = []; | |
/// Replaces all instances of [Map] and [List] in this object with [KeyedArchive] and [ListArchive]s. | |
ListArchive.from(List<dynamic> raw) : _inner = raw.map(_toAtchiveType).toList(); | |
@override | |
dynamic operator [](int index) => _inner[index]; | |
@override | |
int get length => _inner.length; | |
@override | |
set length(int length) { | |
_inner.length = length; | |
} | |
@override | |
void operator []=(int index, dynamic val) { | |
_inner[index] = val; | |
} | |
@override | |
void add(dynamic element) { | |
_inner.add(element); | |
} | |
@override | |
void addAll(Iterable<dynamic> iterable) { | |
_inner.addAll(iterable); | |
} | |
List<dynamic> toPrimitive() { | |
final out = []; | |
for (final val in _inner) { | |
if (val is KeyedArchive) { | |
out.add(val.toPrimitive()); | |
} else if (val is ListArchive) { | |
out.add(val.toPrimitive()); | |
} else { | |
out.add(val); | |
} | |
} | |
return out; | |
} | |
@override | |
void resolveOrThrow(ReferenceResolver coder) { | |
for (final i in _inner) { | |
if (i is KeyedArchive) { | |
i.resolveOrThrow(coder); | |
} else if (i is ListArchive) { | |
i.resolveOrThrow(coder); | |
} | |
} | |
} | |
} | |
dynamic _toAtchiveType(dynamic e) { | |
if (e is Map<String, dynamic>) { | |
return KeyedArchive(e); | |
} else if (e is List) { | |
return ListArchive.from(e); | |
} | |
return e; | |
} |
And Resolver:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// | |
// Modified from original by codable BSD 2 clause | |
// license 2019. | |
import 'package:json_helpers/json_helpers/keyed_archive.dart'; | |
class ReferenceResolver { | |
ReferenceResolver(this.document); | |
final KeyedArchive document; | |
/// resolves a reference of the form '#/yyy/xxx' | |
/// To the value stored in a document | |
/// | |
/// e.g. | |
/// if [ref] == '#/definitions/child' then we would | |
/// return a [KeyedArchive] with the child named Sally. | |
/// | |
/// ``` | |
/// { | |
/// "definitions": { | |
/// "child": {"name": "Sally"} | |
/// }, | |
/// "root": { | |
/// "name": "Bob", | |
/// "child": {"\$ref": "#/definitions/child"} | |
///} | |
/// ``` | |
KeyedArchive? resolve(Uri ref) { | |
final folded = ref.pathSegments.fold<KeyedArchive?>(document, (KeyedArchive? objectPtr, pathSegment) { | |
if (objectPtr != null) { | |
return objectPtr[pathSegment] as KeyedArchive?; | |
} else { | |
return null; | |
} | |
}); | |
return folded; | |
} | |
} |
Now, let me show you how to use it.
Usage
So in a data model class we extend the Coding class:
class Person extends Coding { | |
String name; | |
@override | |
void decode(KeyedArchive object) { | |
// must call super | |
super.decode(object); | |
name = object.decode("name"); | |
} | |
@override | |
void encode(KeyedArchive object) { | |
object.encode("name", name); | |
} | |
} |
That also means for packages like Equatable we use the Equatable mixin. And we can do reading and writing JSON from that data class as:
// decode | |
final json = json.decode(...); | |
final archive = KeyedArchive.unarchive(json); | |
final person = Person()..decode(archive); | |
//encode | |
final person = Person()..name = "Bob"; | |
final archive = KeyedArchive.archive(person); | |
final json = json.encode(archive); |
Using lists in a data class would be something like this:
class Team extends Coding { | |
List<Person> members; | |
Person manager; | |
@override | |
void decode(KeyedArchive object) { | |
super.decode(object); // must call super | |
members = object.decodeObjects("members", () => Person()); | |
manager = object.decodeObject("manager", () => Person()); | |
} | |
@override | |
void encode(KeyedArchive object) { | |
object.encodeObject("manager", manager); | |
object.encodeObjects("members", members); | |
} | |
} |
And the trick to get dynamic type casting you import the cast dart file as the prefix cast and then you can do this:
class Container extends Coding { | |
List<String> things; | |
@override | |
Map<String, cast.Cast<dynamic>> get castMap => { | |
"things": cast.List(cast.String) | |
}; | |
@override | |
void decode(KeyedArchive object) { | |
super.decode(object); | |
things = object.decode("things"); | |
} | |
@override | |
void encode(KeyedArchive object) { | |
object.encode("things", things); | |
} | |
} |
Conclusion
So that is how to encode and decode JSON without having to resort to using code generation. I am keeping these Clean Architecture parts as free articles as there is a lot of messed up ideas out there of how to implement Clean Architecture in flutter.
Upcoming is how do we treat failures in the data layer of clean architecture along with entities, models, repositories, and use cases. And mixed in with that is some smart ways to use functional programming to reduce boilerplate for example in the use case classes.