flattenJson method

Map<String, dynamic> flattenJson(
  1. Map<String, dynamic> input, {
  2. String separator = '.',
})

Flattens a nested JSON object into a single-level map with string keys.

The function can handle nested structures containing other Maps and Lists. In the case of Lists, the list indices are included in the flattened keys.

Parameters:

  • input The nested Map to be flattened. This map can contain other maps, lists, and basic data types (e.g., String, int, bool).
  • separator A string used to separate the segments of the path in the keys of the resulting flat map. Defaults to ..

Returns:

A Map where each key is a path composed of the keys from the original nested map (and indices for lists), separated by separator, leading to the corresponding value.

Example Usage:

final nestedJson = {
  'user': {
    'name': 'Phillip Sherman',
    'address': {
      'street': '42 Wallaby Way',
      'city': 'Sydney',
      'zip': '2000'
    }
  },
  'emails': [
    'p.sherman@sydneydental.com.au',
    'phillip.sherman@gmail.com'
  ]
};

final flattened = flattenJson(nestedJson);
print(flattened);
// Output:
// {
//   'user.name': 'hillip Sherman',
//   'user.address.street': '42 Wallaby Way',
//   'user.address.city': 'Sydney',
//   'user.address.zip': '2000',
//   'emails.0': 'p.sherman@sydneydental.com.au',
//   'emails.1': 'phillip.sherman@gmail.com'
// }

Implementation

Map<String, dynamic> flattenJson(
  Map<String, dynamic> input, {
  String separator = '.',
}) {
  Map<String, dynamic> $flattenJson(dynamic input, [String prefix = '']) {
    final result = <String, dynamic>{};
    void put(String path, dynamic value) {
      // Detect silent collisions where two different source paths map to
      // the same flat key — this would be a data-loss bug for any caller.
      if (result.containsKey(path)) {
        throw StateError(
          '[JsonUtility.flattenJson] Path collision at "$path". Two distinct '
          'source paths produced the same flat key. This usually means a '
          'key in the input contains the separator "$separator", or a '
          'nested structure conflicts with a literal dotted key.',
        );
      }
      result[path] = value;
    }

    void flatten(String path, dynamic value) {
      if (value is Map) {
        if (value.isEmpty) {
          if (path.isNotEmpty) put(path, value);
          return;
        }
        for (final entry in value.entries) {
          final k = entry.key;
          final v = entry.value;
          final keyStr = k.toString();
          if (keyStr.contains(separator)) {
            throw StateError(
              '[JsonUtility.flattenJson] Source key "$keyStr" contains the '
              'separator "$separator"; flattening would produce an '
              'ambiguous path. Pick a separator that does not appear in '
              'any key.',
            );
          }
          final newPath = path.isEmpty ? keyStr : '$path$separator$keyStr';
          flatten(newPath, v);
        }
      } else if (value is List) {
        if (value.isEmpty) {
          if (path.isNotEmpty) put(path, value);
          return;
        }
        for (var i = 0; i < value.length; i++) {
          final newPath = path.isEmpty ? '$i' : '$path$separator$i';
          flatten(newPath, value[i]);
        }
      } else {
        put(path, value);
      }
    }

    flatten(prefix, input);
    return result;
  }

  return $flattenJson(input);
}