Sometimes, when traversing complex json files in Dart it would be nice if we could tell our editor what the expected structure is so that we can make best use of the editor's intelligent code completion features.
As a toy example, consider the script writer.dart:
import 'dart:convert' show json;
main() {
Map<String, num> myMap = {"a": 1, "b": 2, "c": 3};
print(json.encode(myMap));
}
Let's say we use writer.dart to create a json file:
dart writer.dart > data.json
We have another script, called reader.dart, that will read this file and ideally interpret the data as a Map<String, num>
instance:
import 'dart:io' show File;
import 'dart:convert' show json;
main() async {
Map<String, num> myMap = json.decode(await File("data.json").readAsString());
}
This script, however, throws the exception type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Map<String, num>'
.
Naive attempts such as the following also don't work:
var myMap = json.decode(await File("data.json").readAsString()) as Map<String, num>;
Of course we could do something like:
var myMap = Map<String, num>();
(json.decode(await File("data.json").readAsString()) as Map).forEach((k, v) {
myMap[k] = v;
});
But that's really ugly!
What is the best way to let the editor know what data structure to expect when parsing json?
The map returned by json.decode
is a mutable Map<String, Object>
. In this case, you know that the values are all num
instances, but the JSON decoder didn't know that. Even if it did, since the value is mutable, the decoder has to assume someone might want to add any object to the map later.
There are two basic ways to get a map with a stricter type from another map: Copying to a new map (and checking the stricter type while copying), or wrapping/adapting the original map (and checking the stricter type on every access).
You create a new map of whatever type you want by:
Map<String, num> newMap = Map<String, num>.from(originalMap);
The argument to Map.from
is a Map<dynamic, dynamic>
, so it really accepts any map, and it then copies each entry into a new map with the requested type.
If you change the original map after making the copy, the copy is unaffected.
The other option is to wrap using:
Map<String, num> wrappedMap = originalMap.cast<String, num>();
This does nothing up-front. Every time you try to look something up in the wrapped
map, it checks the argument type and return value type against the types you asked for. If you change the original map, the wrapped map changes as well (which is another reason it can't just check the types once, it has to check every time in case the value has changed).
The two approaches have different efficiency trade-offs. Copying everything takes space and time. Checking types takes time too. If you plan to use the same map over and over again, or to modify it, then creating a new map is probably more efficient. If not, Map.cast
is easier to use.
(Although this answer seems obvious in retrospect, I was struggling with this for a while and only figured it out while composing the above question on this site. Just in case it is useful to anyone else, I went ahead and posted the question anyway.)
Just use the from
constructor!
var myMap = Map<String, num>.from(json.decode(await File("data.json").readAsString()));