add a JsonInfoCodec class

This separates the serialization format from the Info objects themselves
and allows us to explore other serialization formats in the future.

BUG=
R=sigmund@google.com

Review URL: https://codereview.chromium.org//1411523003 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f374723..513b704 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 # Changelog
 
 ## 0.2.0
+- Added AllInfoJsonCodec
 - Added `verify_deps` tool
 
 ## 0.1.0
diff --git a/README.md b/README.md
index c5d01bf..b2f53b7 100644
--- a/README.md
+++ b/README.md
@@ -37,8 +37,9 @@
 
 ## Info API
 
-[AllInfo][AllInfo] exposes a Dart representation of the `.info.json` files.
-You can parse the information using `AllInfo.fromJson`. For example:
+[AllInfo][AllInfo] exposes a Dart representation of all of the collected
+information. You can decode an `AllInfo` object from the JSON form produced by
+the `dart2js` `--dump-info` option using the `AllInfoJsonCodec`. For example:
 
 ```dart
 import 'dart:convert';
@@ -49,7 +50,7 @@
 main(args) {
   var infoPath = args[0];
   var json = JSON.decode(new File(infoPath).readAsStringSync());
-  var info = new AllInfo.fromJson(json);
+  var info = new AllInfoJsonCodec().decode(json);
   ...
 }
 ```
diff --git a/bin/code_deps.dart b/bin/code_deps.dart
index 4395526..9899e8d 100644
--- a/bin/code_deps.dart
+++ b/bin/code_deps.dart
@@ -49,7 +49,7 @@
     print('error: could not read ${args[0]}');
     exit(1);
   }
-  var info = new AllInfo.fromJson(json);
+  var info = new AllInfoJsonCodec().decode(json);
   var graph = graphFromInfo(info);
 
   var queryName = args[1];
diff --git a/bin/debug_info.dart b/bin/debug_info.dart
index b8c5025..1c34cca 100644
--- a/bin/debug_info.dart
+++ b/bin/debug_info.dart
@@ -21,7 +21,7 @@
 
   var filename = args[0];
   var json = JSON.decode(new File(filename).readAsStringSync());
-  var info = new AllInfo.fromJson(json);
+  var info = new AllInfoJsonCodec().decode(json);
   var debugLibName;
 
   if (args.length > 2 && args[1] == '--show-library') {
diff --git a/bin/function_size_analysis.dart b/bin/function_size_analysis.dart
index ee10def..0de3db4 100644
--- a/bin/function_size_analysis.dart
+++ b/bin/function_size_analysis.dart
@@ -16,7 +16,7 @@
 
 main(args) {
   var json = JSON.decode(new File(args[0]).readAsStringSync());
-  var info = new AllInfo.fromJson(json);
+  var info = new AllInfoJsonCodec().decode(json);
   showCodeDistribution(info);
 }
 
diff --git a/bin/inference/client.dart b/bin/inference/client.dart
index 0674653..e5b05d7 100644
--- a/bin/inference/client.dart
+++ b/bin/inference/client.dart
@@ -13,8 +13,8 @@
 
 AllInfo data;
 main() async {
-  data =
-      new AllInfo.fromJson(JSON.decode(await HttpRequest.getString('/data')));
+  data = new AllInfoJsonCodec()
+      .decode(JSON.decode(await HttpRequest.getString('/data')));
 
   routeByHash();
   window.onHashChange.listen((_) => routeByHash());
diff --git a/bin/inference/print_summary.dart b/bin/inference/print_summary.dart
index c2926e0..a0dbc0b 100644
--- a/bin/inference/print_summary.dart
+++ b/bin/inference/print_summary.dart
@@ -13,7 +13,7 @@
 main(args) {
   var file = args.length > 0 ? args[0] : 'out.js.info.json';
   var json = JSON.decode(new File(file).readAsStringSync());
-  var results = new AllInfo.fromJson(json);
+  var results = new AllInfoJsonCodec().decode(json);
   print(formatAsTable(results));
 }
 
diff --git a/bin/library_size_split.dart b/bin/library_size_split.dart
index 6228085..8dd419a 100644
--- a/bin/library_size_split.dart
+++ b/bin/library_size_split.dart
@@ -75,7 +75,7 @@
 
   var filename = args[0];
   var json = JSON.decode(new File(filename).readAsStringSync());
-  var info = new AllInfo.fromJson(json);
+  var info = new AllInfoJsonCodec().decode(json);
 
   var groupingText =
       args.length > 1 ? new File(args[1]).readAsStringSync() : defaultGrouping;
diff --git a/bin/live_code_size_analysis.dart b/bin/live_code_size_analysis.dart
index def29d5..333f5c4 100644
--- a/bin/live_code_size_analysis.dart
+++ b/bin/live_code_size_analysis.dart
@@ -49,7 +49,7 @@
   }
 
   var json = JSON.decode(new File(args[0]).readAsStringSync());
-  var info = new AllInfo.fromJson(json);
+  var info = new AllInfoJsonCodec().decode(json);
   var coverage = JSON.decode(new File(args[1]).readAsStringSync());
   var verbose = args.length > 2 && args[2] == '-v';
 
diff --git a/bin/verify_deps.dart b/bin/verify_deps.dart
index 3da8ece..61542c7 100644
--- a/bin/verify_deps.dart
+++ b/bin/verify_deps.dart
@@ -24,7 +24,7 @@
     exit(1);
   }
   var json = JSON.decode(await new File(args[0]).readAsString());
-  var info = new AllInfo.fromJson(json);
+  var info = new AllInfoJsonCodec().decode(json);
   var graph = graphFromInfo(info);
   var entrypoint = info.program.entrypoint;
   var reachables = findReachable(graph, entrypoint);
diff --git a/lib/info.dart b/lib/info.dart
index 27e1fe7..5e79a70 100644
--- a/lib/info.dart
+++ b/lib/info.dart
@@ -5,9 +5,13 @@
 /// Data produced by dart2js when run with the `--dump-info` flag.
 library dart2js_info.info;
 
+import 'dart:convert';
+
 import 'src/measurements.dart';
 export 'src/measurements.dart';
 
+part 'json_info_codec.dart';
+
 /// Common interface to many pieces of information generated by the dart2js
 /// compiler that are directly associated with an element (compilation unit,
 /// library, class, function, or field).
@@ -35,12 +39,7 @@
   /// Info of the enclosing element.
   Info parent;
 
-  /// Serializes the information into a JSON format.
-  // TODO(sigmund): refactor and put toJson outside the class, so we can have 2
-  // different serializer/deserializers at once.
-  Map toJson();
-
-  void accept(InfoVisitor visitor);
+  dynamic accept(InfoVisitor visitor);
 }
 
 /// Common information used for most kind of elements.
@@ -68,21 +67,6 @@
       : kind = _kindFromSerializedId(serializedId),
         id = _idFromSerializedId(serializedId);
 
-  Map toJson() {
-    var res = {
-      'id': serializedId,
-      'kind': _kindToString(kind),
-      'name': name,
-      'size': size,
-    };
-    // TODO(sigmund): Omit this also when outputUnit.id == 0 (most code is in
-    // the main output unit by default).
-    if (outputUnit != null) res['outputUnit'] = outputUnit.serializedId;
-    if (coverageId != null) res['coverageId'] = coverageId;
-    if (parent != null) res['parent'] = parent.serializedId;
-    return res;
-  }
-
   String toString() => '$serializedId $name [$size]';
 }
 
@@ -150,57 +134,7 @@
 
   AllInfo();
 
-  // TODO(het): Remove this when we have an external InfoCodec, see
-  // https://github.com/dart-lang/dart2js_info/issues/4
-  factory AllInfo.fromJson(Map json) => new _ParseHelper().parseAll(json);
-
-  Map _listAsJsonMap(List<Info> list) {
-    var map = <String, Map>{};
-    for (var info in list) {
-      map['${info.id}'] = info.toJson();
-    }
-    return map;
-  }
-
-  Map _extractHoldingInfo() {
-    var map = <String, List>{};
-    void helper(CodeInfo info) {
-      if (info.uses.isEmpty) return;
-      map[info.serializedId] = info.uses.map((u) => u.toJson()).toList();
-    }
-    functions.forEach(helper);
-    fields.forEach(helper);
-    return map;
-  }
-
-  Map _extractDependencies() {
-    var map = <String, List>{};
-    dependencies.forEach((k, v) {
-      map[k.serializedId] = v.map((i) => i.serializedId).toList();
-    });
-    return map;
-  }
-
-  Map toJson() => {
-        'elements': {
-          'library': _listAsJsonMap(libraries),
-          'class': _listAsJsonMap(classes),
-          'function': _listAsJsonMap(functions),
-          'typedef': _listAsJsonMap(typedefs),
-          'field': _listAsJsonMap(fields),
-          'constant': _listAsJsonMap(constants),
-        },
-        'holding': _extractHoldingInfo(),
-        'dependencies': _extractDependencies(),
-        'outputUnits': outputUnits.map((u) => u.toJson()).toList(),
-        'dump_version': version,
-        'deferredFiles': deferredFiles,
-        'dump_minor_version': '$minorVersion',
-        // TODO(sigmund): change viewer to accept an int?
-        'program': program.toJson(),
-      };
-
-  void accept(InfoVisitor visitor) => visitor.visitAll(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitAll(this);
 }
 
 class ProgramInfo {
@@ -226,216 +160,7 @@
       this.noSuchMethodEnabled,
       this.minified});
 
-  Map toJson() => {
-        'entrypoint': entrypoint.serializedId,
-        'size': size,
-        'dart2jsVersion': dart2jsVersion,
-        'compilationMoment': '$compilationMoment',
-        'compilationDuration': '${compilationDuration}',
-        'toJsonDuration': toJsonDuration,
-        'dumpInfoDuration': '$dumpInfoDuration',
-        'noSuchMethodEnabled': noSuchMethodEnabled,
-        'minified': minified,
-      };
-
-  void accept(InfoVisitor visitor) => visitor.visitProgram(this);
-}
-
-// TODO(sigmund): add unit tests.
-class _ParseHelper {
-  Map<String, Info> registry = {};
-
-  AllInfo parseAll(Map json) {
-    var result = new AllInfo();
-    var elements = json['elements'];
-    result.libraries.addAll(elements['library'].values.map(parseLibrary));
-    result.classes.addAll(elements['class'].values.map(parseClass));
-    result.functions.addAll(elements['function'].values.map(parseFunction));
-    result.fields.addAll(elements['field'].values.map(parseField));
-    result.typedefs.addAll(elements['typedef'].values.map(parseTypedef));
-
-    // TODO(sigmund): remove null check on next breaking version
-    var constants = elements['constant'];
-    if (constants != null) {
-      result.constants.addAll(constants.values.map(parseConstant));
-    }
-
-    var idMap = {};
-    for (var f in result.functions) {
-      idMap[f.serializedId] = f;
-    }
-    for (var f in result.fields) {
-      idMap[f.serializedId] = f;
-    }
-
-    json['holding'].forEach((k, deps) {
-      var src = idMap[k];
-      assert(src != null);
-      for (var dep in deps) {
-        var target = idMap[dep['id']];
-        assert(target != null);
-        src.uses.add(new DependencyInfo(target, dep['mask']));
-      }
-    });
-
-    json['dependencies']?.forEach((k, deps) {
-      result.dependencies[idMap[k]] = deps.map((d) => idMap[d]).toList();
-    });
-
-    result.program = parseProgram(json['program']);
-    // todo: version, etc
-    return result;
-  }
-
-  LibraryInfo parseLibrary(Map json) {
-    LibraryInfo result = parseId(json['id']);
-    result
-      ..name = json['name']
-      ..uri = Uri.parse(json['canonicalUri'])
-      ..outputUnit = parseId(json['outputUnit'])
-      ..size = json['size'];
-    for (var child in json['children'].map(parseId)) {
-      if (child is FunctionInfo) {
-        result.topLevelFunctions.add(child);
-      } else if (child is FieldInfo) {
-        result.topLevelVariables.add(child);
-      } else if (child is ClassInfo) {
-        result.classes.add(child);
-      } else {
-        assert(child is TypedefInfo);
-        result.typedefs.add(child);
-      }
-    }
-    return result;
-  }
-
-  ClassInfo parseClass(Map json) {
-    ClassInfo result = parseId(json['id']);
-    result
-      ..name = json['name']
-      ..parent = parseId(json['parent'])
-      ..outputUnit = parseId(json['outputUnit'])
-      ..size = json['size']
-      ..isAbstract = json['modifiers']['abstract'] == true;
-    assert(result is ClassInfo);
-    for (var child in json['children'].map(parseId)) {
-      if (child is FunctionInfo) {
-        result.functions.add(child);
-      } else {
-        assert(child is FieldInfo);
-        result.fields.add(child);
-      }
-    }
-    return result;
-  }
-
-  FieldInfo parseField(Map json) {
-    FieldInfo result = parseId(json['id']);
-    return result
-      ..name = json['name']
-      ..parent = parseId(json['parent'])
-      ..coverageId = json['coverageId']
-      ..outputUnit = parseId(json['outputUnit'])
-      ..size = json['size']
-      ..type = json['type']
-      ..inferredType = json['inferredType']
-      ..code = json['code']
-      ..isConst = json['const'] ?? false
-      ..initializer = parseId(json['initializer'])
-      ..closures = json['children'].map(parseId).toList();
-  }
-
-  ConstantInfo parseConstant(Map json) {
-    ConstantInfo result = parseId(json['id']);
-    return result
-      ..code = json['code']
-      ..size = json['size'];
-  }
-
-  TypedefInfo parseTypedef(Map json) {
-    TypedefInfo result = parseId(json['id']);
-    return result
-      ..name = json['name']
-      ..parent = parseId(json['parent'])
-      ..type = json['type']
-      ..size = 0;
-  }
-
-  ProgramInfo parseProgram(Map json) => new ProgramInfo()
-    ..size = json['size']
-    ..entrypoint = parseId(json['entrypoint']);
-
-  FunctionInfo parseFunction(Map json) {
-    FunctionInfo result = parseId(json['id']);
-    return result
-      ..name = json['name']
-      ..parent = parseId(json['parent'])
-      ..coverageId = json['coverageId']
-      ..outputUnit = parseId(json['outputUnit'])
-      ..size = json['size']
-      ..type = json['type']
-      ..returnType = json['returnType']
-      ..inferredReturnType = json['inferredReturnType']
-      ..parameters = json['parameters'].map(parseParameter).toList()
-      ..code = json['code']
-      ..sideEffects = json['sideEffects']
-      ..modifiers = parseModifiers(json['modifiers'])
-      ..closures = json['children'].map(parseId).toList()
-      ..measurements = parseMeasurements(json['measurements']);
-  }
-
-  ParameterInfo parseParameter(Map json) =>
-      new ParameterInfo(json['name'], json['type'], json['declaredType']);
-
-  Measurements parseMeasurements(Map json) {
-    if (json == null) return null;
-    var uri = json['sourceFile'];
-    var res = new Measurements(uri == null ? null : Uri.parse(uri));
-    for (var key in json.keys) {
-      var value = json[key];
-      if (value == null) continue;
-      if (key == 'entries') {
-        value.forEach((metricName, entries) {
-          var metric = Metric.fromJson(metricName);
-          for (var i = 0; i < entries.length; i += 2) {
-            res.record(metric, entries[i], entries[i + 1]);
-          }
-        });
-      } else {
-        res.counters[Metric.fromJson(key)] = value;
-      }
-    }
-    return res;
-  }
-
-  FunctionModifiers parseModifiers(Map<String, bool> json) {
-    return new FunctionModifiers(
-        isStatic: json['static'] == true,
-        isConst: json['const'] == true,
-        isFactory: json['factory'] == true,
-        isExternal: json['external'] == true);
-  }
-
-  Info parseId(String serializedId) => registry.putIfAbsent(serializedId, () {
-        if (serializedId == null) {
-          return null;
-        } else if (serializedId.startsWith('function/')) {
-          return new FunctionInfo._(serializedId);
-        } else if (serializedId.startsWith('library/')) {
-          return new LibraryInfo._(serializedId);
-        } else if (serializedId.startsWith('class/')) {
-          return new ClassInfo._(serializedId);
-        } else if (serializedId.startsWith('field/')) {
-          return new FieldInfo._(serializedId);
-        } else if (serializedId.startsWith('constant/')) {
-          return new ConstantInfo._(serializedId);
-        } else if (serializedId.startsWith('typedef/')) {
-          return new TypedefInfo._(serializedId);
-        } else if (serializedId.startsWith('outputUnit/')) {
-          return new OutputUnitInfo._(serializedId);
-        }
-        assert(false);
-      });
+  dynamic accept(InfoVisitor visitor) => visitor.visitProgram(this);
 }
 
 /// Info associated with a library element.
@@ -470,17 +195,7 @@
 
   LibraryInfo._(String serializedId) : super._fromId(serializedId);
 
-  Map toJson() => super.toJson()
-    ..addAll({
-      'children': []
-        ..addAll(topLevelFunctions.map((f) => f.serializedId))
-        ..addAll(topLevelVariables.map((v) => v.serializedId))
-        ..addAll(classes.map((c) => c.serializedId))
-        ..addAll(typedefs.map((t) => t.serializedId)),
-      'canonicalUri': '$uri',
-    });
-
-  void accept(InfoVisitor visitor) => visitor.visitLibrary(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitLibrary(this);
 }
 
 /// Information about an output unit. Normally there is just one for the entire
@@ -493,7 +208,7 @@
 
   OutputUnitInfo._(String serializedId) : super._fromId(serializedId);
 
-  void accept(InfoVisitor visitor) => visitor.visitOutput(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitOutput(this);
 }
 
 /// Information about a class element.
@@ -517,16 +232,7 @@
 
   ClassInfo._(String serializedId) : super._fromId(serializedId);
 
-  Map toJson() => super.toJson()
-    ..addAll({
-      // TODO(sigmund): change format, include only when abstract is true.
-      'modifiers': {'abstract': isAbstract},
-      'children': []
-        ..addAll(fields.map((f) => f.serializedId))
-        ..addAll(functions.map((m) => m.serializedId))
-    });
-
-  void accept(InfoVisitor visitor) => visitor.visitClass(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitClass(this);
 }
 
 /// Information about a constant value.
@@ -542,9 +248,7 @@
 
   ConstantInfo._(String serializedId) : super._fromId(serializedId);
 
-  Map toJson() => super.toJson()..addAll({'code': code});
-
-  void accept(InfoVisitor visitor) => visitor.visitConstant(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitConstant(this);
 }
 
 /// Information about a field element.
@@ -582,22 +286,7 @@
 
   FieldInfo._(String serializedId) : super._fromId(serializedId);
 
-  Map toJson() {
-    var result = super.toJson()
-      ..addAll({
-        'children': closures.map((i) => i.serializedId).toList(),
-        'inferredType': inferredType,
-        'code': code,
-        'type': type,
-      });
-    if (isConst) {
-      result['const'] = true;
-      if (initializer != null) result['initializer'] = initializer.serializedId;
-    }
-    return result;
-  }
-
-  void accept(InfoVisitor visitor) => visitor.visitField(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitField(this);
 }
 
 /// Information about a typedef declaration.
@@ -611,9 +300,7 @@
 
   TypedefInfo._(String serializedId) : super._fromId(serializedId);
 
-  Map toJson() => super.toJson()..['type'] = '$type';
-
-  void accept(InfoVisitor visitor) => visitor.visitTypedef(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitTypedef(this);
 }
 
 /// Information about a function or method.
@@ -678,23 +365,7 @@
 
   FunctionInfo._(String serializedId) : super._fromId(serializedId);
 
-  Map toJson() => super.toJson()
-    ..addAll({
-      'children': closures.map((i) => i.serializedId).toList(),
-      'modifiers': modifiers.toJson(),
-      'returnType': returnType,
-      'inferredReturnType': inferredReturnType,
-      'parameters': parameters.map((p) => p.toJson()).toList(),
-      'sideEffects': sideEffects,
-      'inlinedCount': inlinedCount,
-      'code': code,
-      'type': type,
-      'measurements': measurements?.toJson(),
-      // Note: version 3.2 of dump-info serializes `uses` in a section called
-      // `holding` at the top-level.
-    });
-
-  void accept(InfoVisitor visitor) => visitor.visitFunction(this);
+  dynamic accept(InfoVisitor visitor) => visitor.visitFunction(this);
 }
 
 /// Information about how a dependency is used.
@@ -708,8 +379,6 @@
   final String mask;
 
   DependencyInfo(this.target, this.mask);
-
-  Map toJson() => {'id': target.serializedId, 'mask': mask};
 }
 
 /// Name and type information about a function parameter.
@@ -719,8 +388,6 @@
   final String declaredType;
 
   ParameterInfo(this.name, this.type, this.declaredType);
-
-  Map toJson() => {'name': name, 'type': type, 'declaredType': declaredType};
 }
 
 /// Modifiers that may apply to methods.
@@ -735,22 +402,6 @@
       this.isConst: false,
       this.isFactory: false,
       this.isExternal: false});
-
-  // TODO(sigmund): exclude false values (requires bumping the format version):
-  //   Map toJson() {
-  //     var res = <String, bool>{};
-  //     if (isStatic) res['static'] = true;
-  //     if (isConst) res['const'] = true;
-  //     if (isFactory) res['factory'] = true;
-  //     if (isExternal) res['external'] = true;
-  //     return res;
-  //   }
-  Map toJson() => {
-        'static': isStatic,
-        'const': isConst,
-        'factory': isFactory,
-        'external': isExternal,
-      };
 }
 
 /// Possible values of the `kind` field in the serialied infos.
@@ -813,16 +464,16 @@
 }
 
 /// A simple visitor for information produced by the dart2js compiler.
-class InfoVisitor {
-  visitAll(AllInfo info) {}
-  visitProgram(ProgramInfo info) {}
-  visitLibrary(LibraryInfo info) {}
-  visitClass(ClassInfo info) {}
-  visitField(FieldInfo info) {}
-  visitConstant(ConstantInfo info) {}
-  visitFunction(FunctionInfo info) {}
-  visitTypedef(TypedefInfo info) {}
-  visitOutput(OutputUnitInfo info) {}
+abstract class InfoVisitor<T> {
+  T visitAll(AllInfo info);
+  T visitProgram(ProgramInfo info);
+  T visitLibrary(LibraryInfo info);
+  T visitClass(ClassInfo info);
+  T visitField(FieldInfo info);
+  T visitConstant(ConstantInfo info);
+  T visitFunction(FunctionInfo info);
+  T visitTypedef(TypedefInfo info);
+  T visitOutput(OutputUnitInfo info);
 }
 
 /// A visitor that recursively walks each portion of the program. Because the
@@ -831,7 +482,7 @@
 /// visitAll contains references to functions, this visitor only recurses to
 /// visit libraries, then from each library we visit functions and classes, and
 /// so on.
-class RecursiveInfoVisitor extends InfoVisitor {
+class RecursiveInfoVisitor extends InfoVisitor<Null> {
   visitAll(AllInfo info) {
     // Note: we don't visit functions, fields, classes, and typedefs because
     // they are reachable from the library info.
@@ -839,6 +490,8 @@
     info.constants.forEach(visitConstant);
   }
 
+  visitProgram(ProgramInfo info) {}
+
   visitLibrary(LibraryInfo info) {
     info.topLevelFunctions.forEach(visitFunction);
     info.topLevelVariables.forEach(visitField);
@@ -855,7 +508,12 @@
     info.closures.forEach(visitFunction);
   }
 
+  visitConstant(ConstantInfo info) {}
+
   visitFunction(FunctionInfo info) {
     info.closures.forEach(visitFunction);
   }
+
+  visitTypedef(TypedefInfo info) {}
+  visitOutput(OutputUnitInfo info) {}
 }
diff --git a/lib/json_info_codec.dart b/lib/json_info_codec.dart
new file mode 100644
index 0000000..1293d30
--- /dev/null
+++ b/lib/json_info_codec.dart
@@ -0,0 +1,415 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Converters and codecs for converting between JSON and [Info] classes.
+part of dart2js_info.info;
+
+// TODO(sigmund): add unit tests.
+class JsonToAllInfoConverter extends Converter<Map, AllInfo> {
+  Map<String, Info> registry;
+
+  AllInfo convert(Map json) {
+    registry = <String, Info>{};
+
+    var result = new AllInfo();
+    var elements = json['elements'];
+    result.libraries.addAll(elements['library'].values.map(parseLibrary));
+    result.classes.addAll(elements['class'].values.map(parseClass));
+    result.functions.addAll(elements['function'].values.map(parseFunction));
+    result.fields.addAll(elements['field'].values.map(parseField));
+    result.typedefs.addAll(elements['typedef'].values.map(parseTypedef));
+
+    // TODO(sigmund): remove null check on next breaking version
+    var constants = elements['constant'];
+    if (constants != null) {
+      result.constants.addAll(constants.values.map(parseConstant));
+    }
+
+    var idMap = {};
+    for (var f in result.functions) {
+      idMap[f.serializedId] = f;
+    }
+    for (var f in result.fields) {
+      idMap[f.serializedId] = f;
+    }
+
+    json['holding'].forEach((k, deps) {
+      var src = idMap[k];
+      assert(src != null);
+      for (var dep in deps) {
+        var target = idMap[dep['id']];
+        assert(target != null);
+        src.uses.add(new DependencyInfo(target, dep['mask']));
+      }
+    });
+
+    json['dependencies']?.forEach((k, deps) {
+      result.dependencies[idMap[k]] = deps.map((d) => idMap[d]).toList();
+    });
+
+    result.program = parseProgram(json['program']);
+    // todo: version, etc
+    return result;
+  }
+
+  LibraryInfo parseLibrary(Map json) {
+    LibraryInfo result = parseId(json['id']);
+    result
+      ..name = json['name']
+      ..uri = Uri.parse(json['canonicalUri'])
+      ..outputUnit = parseId(json['outputUnit'])
+      ..size = json['size'];
+    for (var child in json['children'].map(parseId)) {
+      if (child is FunctionInfo) {
+        result.topLevelFunctions.add(child);
+      } else if (child is FieldInfo) {
+        result.topLevelVariables.add(child);
+      } else if (child is ClassInfo) {
+        result.classes.add(child);
+      } else {
+        assert(child is TypedefInfo);
+        result.typedefs.add(child);
+      }
+    }
+    return result;
+  }
+
+  ClassInfo parseClass(Map json) {
+    ClassInfo result = parseId(json['id']);
+    result
+      ..name = json['name']
+      ..parent = parseId(json['parent'])
+      ..outputUnit = parseId(json['outputUnit'])
+      ..size = json['size']
+      ..isAbstract = json['modifiers']['abstract'] == true;
+    assert(result is ClassInfo);
+    for (var child in json['children'].map(parseId)) {
+      if (child is FunctionInfo) {
+        result.functions.add(child);
+      } else {
+        assert(child is FieldInfo);
+        result.fields.add(child);
+      }
+    }
+    return result;
+  }
+
+  FieldInfo parseField(Map json) {
+    FieldInfo result = parseId(json['id']);
+    return result
+      ..name = json['name']
+      ..parent = parseId(json['parent'])
+      ..coverageId = json['coverageId']
+      ..outputUnit = parseId(json['outputUnit'])
+      ..size = json['size']
+      ..type = json['type']
+      ..inferredType = json['inferredType']
+      ..code = json['code']
+      ..isConst = json['const'] ?? false
+      ..initializer = parseId(json['initializer'])
+      ..closures = json['children'].map(parseId).toList();
+  }
+
+  ConstantInfo parseConstant(Map json) {
+    ConstantInfo result = parseId(json['id']);
+    return result
+      ..code = json['code']
+      ..size = json['size'];
+  }
+
+  TypedefInfo parseTypedef(Map json) {
+    TypedefInfo result = parseId(json['id']);
+    return result
+      ..name = json['name']
+      ..parent = parseId(json['parent'])
+      ..type = json['type']
+      ..size = 0;
+  }
+
+  ProgramInfo parseProgram(Map json) => new ProgramInfo()
+    ..size = json['size']
+    ..entrypoint = parseId(json['entrypoint']);
+
+  FunctionInfo parseFunction(Map json) {
+    FunctionInfo result = parseId(json['id']);
+    return result
+      ..name = json['name']
+      ..parent = parseId(json['parent'])
+      ..coverageId = json['coverageId']
+      ..outputUnit = parseId(json['outputUnit'])
+      ..size = json['size']
+      ..type = json['type']
+      ..returnType = json['returnType']
+      ..inferredReturnType = json['inferredReturnType']
+      ..parameters = json['parameters'].map(parseParameter).toList()
+      ..code = json['code']
+      ..sideEffects = json['sideEffects']
+      ..modifiers = parseModifiers(json['modifiers'])
+      ..closures = json['children'].map(parseId).toList()
+      ..measurements = parseMeasurements(json['measurements']);
+  }
+
+  ParameterInfo parseParameter(Map json) =>
+      new ParameterInfo(json['name'], json['type'], json['declaredType']);
+
+  Measurements parseMeasurements(Map json) {
+    if (json == null) return null;
+    var uri = json['sourceFile'];
+    var res = new Measurements(uri == null ? null : Uri.parse(uri));
+    for (var key in json.keys) {
+      var value = json[key];
+      if (value == null) continue;
+      if (key == 'entries') {
+        value.forEach((metricName, entries) {
+          var metric = new Metric.fromName(metricName);
+          for (var i = 0; i < entries.length; i += 2) {
+            res.record(metric, entries[i], entries[i + 1]);
+          }
+        });
+      } else {
+        res.counters[new Metric.fromName(key)] = value;
+      }
+    }
+    return res;
+  }
+
+  FunctionModifiers parseModifiers(Map<String, bool> json) {
+    return new FunctionModifiers(
+        isStatic: json['static'] == true,
+        isConst: json['const'] == true,
+        isFactory: json['factory'] == true,
+        isExternal: json['external'] == true);
+  }
+
+  Info parseId(String serializedId) => registry.putIfAbsent(serializedId, () {
+        if (serializedId == null) {
+          return null;
+        } else if (serializedId.startsWith('function/')) {
+          return new FunctionInfo._(serializedId);
+        } else if (serializedId.startsWith('library/')) {
+          return new LibraryInfo._(serializedId);
+        } else if (serializedId.startsWith('class/')) {
+          return new ClassInfo._(serializedId);
+        } else if (serializedId.startsWith('field/')) {
+          return new FieldInfo._(serializedId);
+        } else if (serializedId.startsWith('constant/')) {
+          return new ConstantInfo._(serializedId);
+        } else if (serializedId.startsWith('typedef/')) {
+          return new TypedefInfo._(serializedId);
+        } else if (serializedId.startsWith('outputUnit/')) {
+          return new OutputUnitInfo._(serializedId);
+        }
+        assert(false);
+      });
+}
+
+class AllInfoToJsonConverter extends Converter<AllInfo, Map>
+    implements InfoVisitor<Map> {
+  Map convert(AllInfo info) => info.accept(this);
+
+  Map _visitList(List<Info> infos) {
+    var map = <String, Map>{};
+    for (var info in infos) {
+      map['${info.id}'] = info.accept(this);
+    }
+    return map;
+  }
+
+  Map _visitAllInfoElements(AllInfo info) {
+    var jsonLibraries = _visitList(info.libraries);
+    var jsonClasses = _visitList(info.classes);
+    var jsonFunctions = _visitList(info.functions);
+    var jsonTypedefs = _visitList(info.typedefs);
+    var jsonFields = _visitList(info.fields);
+    var jsonConstants = _visitList(info.constants);
+    return {
+      'library': jsonLibraries,
+      'class': jsonClasses,
+      'function': jsonFunctions,
+      'typedef': jsonTypedefs,
+      'field': jsonFields,
+      'constant': jsonConstants
+    };
+  }
+
+  Map _visitDependencyInfo(DependencyInfo info) =>
+      {'id': info.target.serializedId, 'mask': info.mask};
+
+  Map _visitAllInfoHolding(AllInfo allInfo) {
+    var map = <String, List>{};
+    void helper(CodeInfo info) {
+      if (info.uses.isEmpty) return;
+      map[info.serializedId] =
+          info.uses.map((u) => _visitDependencyInfo(u)).toList();
+    }
+    allInfo.functions.forEach(helper);
+    allInfo.fields.forEach(helper);
+    return map;
+  }
+
+  Map _visitAllInfoDependencies(AllInfo allInfo) {
+    var map = <String, List>{};
+    allInfo.dependencies.forEach((k, v) {
+      map[k.serializedId] = v.map((i) => i.serializedId).toList();
+    });
+    return map;
+  }
+
+  Map visitAll(AllInfo info) {
+    var elements = _visitAllInfoElements(info);
+    var jsonHolding = _visitAllInfoHolding(info);
+    var jsonDependencies = _visitAllInfoDependencies(info);
+    return {
+      'elements': elements,
+      'holding': jsonHolding,
+      'dependencies': jsonDependencies,
+      'outputUnits': info.outputUnits.map((u) => u.accept(this)).toList(),
+      'dump_version': info.version,
+      'deferredFiles': info.deferredFiles,
+      'dump_minor_version': '${info.minorVersion}',
+      'program': info.program.accept(this)
+    };
+  }
+
+  Map visitProgram(ProgramInfo info) {
+    return {
+      'entrypoint': info.entrypoint.serializedId,
+      'size': info.size,
+      'dart2jsVersion': info.dart2jsVersion,
+      'compilationMoment': '${info.compilationMoment}',
+      'compilationDuration': '${info.compilationDuration}',
+      'toJsonDuration': info.toJsonDuration,
+      'dumpInfoDuration': '${info.dumpInfoDuration}',
+      'noSuchMethodEnabled': info.noSuchMethodEnabled,
+      'minified': info.minified,
+    };
+  }
+
+  Map _visitBasicInfo(BasicInfo info) {
+    var res = {
+      'id': info.serializedId,
+      'kind': _kindToString(info.kind),
+      'name': info.name,
+      'size': info.size,
+    };
+    // TODO(sigmund): Omit this also when outputUnit.id == 0 (most code is in
+    // the main output unit by default).
+    if (info.outputUnit != null) res['outputUnit'] =
+        info.outputUnit.serializedId;
+    if (info.coverageId != null) res['coverageId'] = info.coverageId;
+    if (info.parent != null) res['parent'] = info.parent.serializedId;
+    return res;
+  }
+
+  Map visitLibrary(LibraryInfo info) {
+    return _visitBasicInfo(info)
+      ..addAll({
+        'children': []
+          ..addAll(info.topLevelFunctions.map((f) => f.serializedId))
+          ..addAll(info.topLevelVariables.map((v) => v.serializedId))
+          ..addAll(info.classes.map((c) => c.serializedId))
+          ..addAll(info.typedefs.map((t) => t.serializedId)),
+        'canonicalUri': '${info.uri}',
+      });
+  }
+
+  Map visitClass(ClassInfo info) {
+    return _visitBasicInfo(info)
+      ..addAll({
+        // TODO(sigmund): change format, include only when abstract is true.
+        'modifiers': {'abstract': info.isAbstract},
+        'children': []
+          ..addAll(info.fields.map((f) => f.serializedId))
+          ..addAll(info.functions.map((m) => m.serializedId))
+      });
+  }
+
+  Map visitField(FieldInfo info) {
+    var result = _visitBasicInfo(info)
+      ..addAll({
+        'children': info.closures.map((i) => i.serializedId).toList(),
+        'inferredType': info.inferredType,
+        'code': info.code,
+        'type': info.type,
+      });
+    if (info.isConst) {
+      result['const'] = true;
+      if (info.initializer != null) result['initializer'] =
+          info.initializer.serializedId;
+    }
+    return result;
+  }
+
+  Map visitConstant(ConstantInfo info) =>
+      _visitBasicInfo(info)..addAll({'code': info.code});
+
+  // TODO(sigmund): exclude false values (requires bumping the format version):
+  //     var res = <String, bool>{};
+  //     if (isStatic) res['static'] = true;
+  //     if (isConst) res['const'] = true;
+  //     if (isFactory) res['factory'] = true;
+  //     if (isExternal) res['external'] = true;
+  //     return res;
+  Map _visitFunctionModifiers(FunctionModifiers mods) => {
+        'static': mods.isStatic,
+        'const': mods.isConst,
+        'factory': mods.isFactory,
+        'external': mods.isExternal,
+      };
+
+  Map _visitParameterInfo(ParameterInfo info) =>
+      {'name': info.name, 'type': info.type, 'declaredType': info.declaredType};
+
+  String _visitMetric(Metric metric) => metric.name;
+
+  Map _visitMeasurements(Measurements measurements) {
+    if (measurements == null) return null;
+    var jsonEntries = <String, List<Map>>{};
+    measurements.entries.forEach((metric, values) {
+      jsonEntries[_visitMetric(metric)] =
+          values.expand((e) => [e.begin, e.end]).toList();
+    });
+    var json = {'entries': jsonEntries};
+    // TODO(sigmund): encode uri as an offset of the URIs available in the parts
+    // of the library info.
+    if (measurements.uri != null) json['sourceFile'] = '${measurements.uri}';
+    if (measurements.counters[Metric.functions] != null) {
+      json[_visitMetric(Metric.functions)] =
+          measurements.counters[Metric.functions];
+    }
+    if (measurements.counters[Metric.reachableFunctions] != null) {
+      json[_visitMetric(Metric.reachableFunctions)] =
+          measurements.counters[Metric.reachableFunctions];
+    }
+    return json;
+  }
+
+  Map visitFunction(FunctionInfo info) {
+    return _visitBasicInfo(info)
+      ..addAll({
+        'children': info.closures.map((i) => i.serializedId).toList(),
+        'modifiers': _visitFunctionModifiers(info.modifiers),
+        'returnType': info.returnType,
+        'inferredReturnType': info.inferredReturnType,
+        'parameters':
+            info.parameters.map((p) => _visitParameterInfo(p)).toList(),
+        'sideEffects': info.sideEffects,
+        'inlinedCount': info.inlinedCount,
+        'code': info.code,
+        'type': info.type,
+        'measurements': _visitMeasurements(info.measurements),
+        // Note: version 3.2 of dump-info serializes `uses` in a section called
+        // `holding` at the top-level.
+      });
+  }
+
+  visitTypedef(TypedefInfo info) => _visitBasicInfo(info)..['type'] = info.type;
+
+  visitOutput(OutputUnitInfo info) => _visitBasicInfo(info);
+}
+
+class AllInfoJsonCodec extends Codec<AllInfo, Map> {
+  final Converter<AllInfo, Map> encoder = new AllInfoToJsonConverter();
+  final Converter<Map, AllInfo> decoder = new JsonToAllInfoConverter();
+}
diff --git a/lib/src/measurements.dart b/lib/src/measurements.dart
index 499f001..b919678 100644
--- a/lib/src/measurements.dart
+++ b/lib/src/measurements.dart
@@ -7,10 +7,7 @@
 library dart2js_info.src.measurements;
 
 /// Top-level set of metrics
-const List<Metric> _topLevelMetrics = const [
-  Metric.functions,
-  Metric.send,
-];
+const List<Metric> _topLevelMetrics = const [Metric.functions, Metric.send];
 
 /// Apply `f` on each metric in DFS order on the metric tree. [Metric.functions]
 /// and [Metric.send] are the top level metrics. See those declarations for
@@ -35,13 +32,14 @@
 
   const Metric(this.name);
 
+  factory Metric.fromName(String name) => _nameToMetricMap[name];
+
   String toString() => name;
 
   /// Total functions in a library/package/program. Parent of
   /// [reachableFunction].
-  static const Metric functions = const GroupedMetric('functions', const [
-      reachableFunctions,
-  ]);
+  static const Metric functions =
+      const GroupedMetric('functions', const [reachableFunctions]);
 
   /// Subset of the functions that are reachable.
   static const Metric reachableFunctions = const Metric('reachable functions');
@@ -68,24 +66,22 @@
   ///          |- multi-interceptor (1 of n possible interceptors)
   ///          '- dynamic (any combination of the above)
   ///
-  static const Metric send = const GroupedMetric('send', const [
-      monomorphicSend,
-      polymorphicSend,
-  ]);
+  static const Metric send =
+      const GroupedMetric('send', const [monomorphicSend, polymorphicSend]);
 
   /// Parent of monomorphic sends, see [send] for details.
-  static const Metric monomorphicSend = const GroupedMetric('monomorphic',
-      const [
-        staticSend,
-        superSend,
-        localSend,
-        constructorSend,
-        typeVariableSend,
-        nsmErrorSend,
-        singleNsmCallSend,
-        instanceSend,
-        interceptorSend,
-      ]);
+  static const Metric monomorphicSend =
+      const GroupedMetric('monomorphic', const [
+    staticSend,
+    superSend,
+    localSend,
+    constructorSend,
+    typeVariableSend,
+    nsmErrorSend,
+    singleNsmCallSend,
+    instanceSend,
+    interceptorSend
+  ]);
 
   /// Metric for static calls, see [send] for details.
   static const Metric staticSend = const Metric('static');
@@ -120,12 +116,7 @@
 
   /// Parent of polymorphic sends, see [send] for details.
   static const Metric polymorphicSend = const GroupedMetric('polymorphic',
-      const [
-        multiNsmCallSend,
-        virtualSend,
-        multiInterceptorSend,
-        dynamicSend,
-      ]);
+      const [multiNsmCallSend, virtualSend, multiInterceptorSend, dynamicSend]);
 
   /// Metric for calls to noSuchMethod methods with more than one possible
   /// target, see [send] for details.
@@ -143,14 +134,11 @@
   /// method. See [send] for details.
   static const Metric dynamicSend = const Metric('dynamic');
 
-  String toJson() => name;
   static Map<String, Metric> _nameToMetricMap = () {
     var res = {};
     visitAllMetrics((m, _) => res[m.name] = m);
     return res;
   }();
-
-  static Metric fromJson(String name) => _nameToMetricMap[name];
 }
 
 /// A metric that is subdivided in smaller metrics.
@@ -180,10 +168,12 @@
         counters = <Metric, int>{};
 
   const Measurements.unreachableFunction()
-      : counters = const { Metric.functions: 1}, entries = const {}, uri = null;
+      : counters = const {Metric.functions: 1},
+        entries = const {},
+        uri = null;
 
   Measurements.reachableFunction([this.uri])
-      : counters = { Metric.functions: 1, Metric.reachableFunctions: 1},
+      : counters = {Metric.functions: 1, Metric.reachableFunctions: 1},
         entries = {};
 
   /// Record [metric] was seen. The optional [begin] and [end] offsets are
@@ -228,24 +218,4 @@
     }
     return total == submetricTotal;
   }
-
-  Map toJson() {
-    var jsonEntries = <String, List<Map>>{};
-    entries.forEach((metric, values) {
-      jsonEntries[metric.toJson()] =
-          values.expand((e) => [e.begin, e.end]).toList();
-    });
-    var json = {'entries': jsonEntries};
-    // TODO(sigmund): encode uri as an offset of the URIs available in the parts
-    // of the library info.
-    if (uri != null) json['sourceFile'] = '$uri';
-    if (counters[Metric.functions] != null) {
-      json[Metric.functions.toJson()] = counters[Metric.functions];
-    }
-    if (counters[Metric.reachableFunctions] != null) {
-      json[Metric.reachableFunctions.toJson()] =
-          counters[Metric.reachableFunctions];
-    }
-    return json;
-  }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 082d211..ff85bc1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart2js_info
-version: 0.2.0-dev
+version: 0.2.0
 description: >
   Libraries and tools to process data produced when running dart2js with
   --dump-info.
diff --git a/test/all_test.dart b/test/all_test.dart
index 40cdf2e..3dc5546 100644
--- a/test/all_test.dart
+++ b/test/all_test.dart
@@ -21,7 +21,7 @@
         'program': {'size': 10},
       };
 
-      expect(new AllInfo.fromJson(json).program.size, 10);
+      expect(new AllInfoJsonCodec().decode(json).program.size, 10);
     });
   });
 }