flatten function Null safety

List<Map<String, dynamic>> flatten(
  1. dynamic json,
  2. {String separator = '.',
  3. List<String> recordPath = const [],
  4. List<List<String>> includePath = const [],
  5. bool addMissingKeys = true,
  6. int maxLevel = -1}
)

flatten is similar to pandas' json_normalize function, this function recursively flattens the nested JSON objects into a flat tabular list. It returns List<Map<String, dynamic>> of the flattened records.

Simple example:

[
  {"id": 1, "name": {"first": "Coleen", "last": "Volk"}},
  {"name": {"given": "Mark", "family": "Regner"}},
  {"id": 2, "name": "Faye Raker"},
]
flatten(input);

Results in:

[
  {"id": 1, "name.first": "Coleen", "name.last": "Volk", "name.given": null, "name.family": null},
  {"id": null, "name.first": null, "name.last": null, "name.given": "Mark", "name.family": "Regner"},
  {"id": 2, "name": "Faye Raker", "name.first": null, "name.last": null, "name.given": null, "name.family": null},
]

Advanced example:

[
   {
       "state": "Florida",
       "shortname": "FL",
       "info": {"governor": "Rick Scott"},
       "counties": [
           {"name": "Dade", "population": 12345},
           {"name": "Broward", "population": 40000},
           {"name": "Palm Beach", "population": 60000},
       ],
   },
   {
       "state": "Ohio",
       "shortname": "OH",
       "info": {"governor": "John Kasich"},
       "counties": [
           {"name": "Summit", "population": 1234},
           {"name": "Cuyahoga", "population": 1337},
       ],
   },
]
flatten(input, recordPath: ["counties"], includePath: [["state"], ["shortname"], ["info", "governor"]]);

Results in:

[
  {
    "state": "Florida",
    "shortname": "FL",
    "info.governor": "Rick Scott",
    "name": "Dade",
    "population": 12345
  },
  {
    "state": "Florida",
    "shortname": "FL",
    "info.governor": "Rick Scott",
    "name": "Broward",
    "population": 40000
  },
  {
    "state": "Florida",
    "shortname": "FL",
    "info.governor": "Rick Scott",
    "name": "Palm Beach",
    "population": 60000
  },
  {
    "state": "Ohio",
    "shortname": "OH",
    "info.governor": "John Kasich",
    "name": "Summit",
    "population": 1234
  },
  {
    "state": "Ohio",
    "shortname": "OH",
    "info.governor": "John Kasich",
    "name": "Cuyahoga",
    "population": 1337
  },
]

Implementation

List<Map<String, dynamic>> flatten(
  /// Json to flatten
  dynamic json, {
  /// Separator used to join the path of the nested keys
  String separator = '.',

  /// Json path of records in the given json. This will be the entry point for the flattening.
  List<String> recordPath = const [],

  /// Json path from the root of data to extract from the nested records.
  /// Each path will become a column in the resulting table.
  List<List<String>> includePath = const [],

  /// Add missing fields or column to the records.
  bool addMissingKeys = true,

  // TODO: Implement maxLevel
  /// Level of nesting to flatten
  int maxLevel = -1,
}) {
  final result = <Map<String, dynamic>>[];
  final seenKeys = addMissingKeys ? <String>{} : null;

  final includeResult = <String, dynamic>{};
  final includeKeys = includePath.map((i) => i.join(separator)).toSet();

  final commonPath = includePath.isNotEmpty
      ? (includePath
          .map((i) => i
              .headIntersect(recordPath)
              // .whereNot((e) => e == '*')
              .join(separator))
          .toSet())
      : const <String>{};

  // if (kDebugMode) print(includeKeys);
  // if (kDebugMode) print(commonPath);

  _flatten(
    json,
    separator,
    const [],
    recordPath,
    includeKeys,
    commonPath,
    maxLevel,
    listAccumulator(result, seenKeys),
    includeAccumulator(includeResult, includeKeys),
  );

  // if (kDebugMode) print(includeResult);

  if (seenKeys != null) {
    for (final e in result) {
      for (final k in seenKeys) {
        e.putIfAbsent(k, () => null);
      }
    }
  }

  return result;
}