A json String, a json 'Dart', or any maps & lists set, are enough to create a class with a dot notation to access properties.
dynamic p1 = MapListMap({
"name": "Polo",
"firstName": "marco",
"birthDate": { "day": 15, "month": 9, "year": 1254 }
});
print('${p1.firstName} ${p1.name} have now ${DateTime.now().year - p1.birthDate.year} years');
// -> marco Polo have now 766 years
p1.firstName = 'Marco';
// add a collection for business cards
p1.cards = [];
// add a new card with a pre-filled map
p1.cards.add({
"mail": "ma.po.lo@china.com",
});
Continue freely the creation chain
// add to this -last added- map a new entry
p1.cards.last.phone = "+99 01 02 03 04 05";
print(p1.cards.last.mail);
print(p1.cards.last.phone);
//@see examples for more code
At any time the underlying json is updated and available
print(p1.json);
What is available in dot notation within Dart is also available by script :
var scriptLines = [
'persons=[]',
'''persons.add({ "name": "Magellan", "firstName": "Fernando",
"birthDate": { "day": 15,"month": 3,"year": 1480}
})
''',
'persons.last.cards = {"mail": "ma.po.lo@china.com"})',
'persons.last.cards.phone = "+99 01 02 03 04 05"'
];
Script executor is under a MapList responsibility
dynamic myKB = MapListMap();
for (String line in scriptLines) myKB.eval(line);
Resulting data can be accessed by code or by script
print( myKB.persons.last.cards.phone); // code
print(myKB.eval('persons.last.cards.phone')); // interpreter
There is two kinds of structures : MapListMap and MapListList
If you decide by yourself, choose your root within this two options:
dynamic myRootMap = MapListMap(); // empty map { }
dynamic myRootList = MapListList(); // empty list [ ]
If you rely on a variable json , you can leave a higher factory do the choice :
dynamic myRoot = Maplist(someJson); // return a MapListMap or a MapListList
Each constructor can be default as above, or can be initialised with :
- a Json String
- an inline maps and lists in dart
- an already loaded 'Dart Json'
dynamic myRootMap = MapListMap('{"name":"Polo"}'); // string
dynamic myRootMap = MapListMap({"name":"Polo"}); // inline
var myJson = json.decode('{"name":"Polo"}');
dynamic myRootMap = MapListMap(myJson); // already json dart
Same options are for MapListList constructors, but beginning by a List [ ]
Same options with the Factory MapList which will decide of the following.
Classical and dot notation are usable in code and in scripts.
The result is the last leaf which could be a simple data, a List, a Map or a null if not found.
root["show"]["videos"][1]["name"]
root.show.videos[1].name
a .someName indicates a key entry in a map.
- if the result is another Map
- can continue with another key : .someName.someList
- if the result is a List
- can continue with an index : someList[1]
- can use the keyword last : someList.last
- The result of the result can be a Map or another List
- someList [10].anotherKey
- somelist [10] [2]
- The result of the result can be a Map or another List
- if the result is a simple data, cannot continue notation : must be the last leaf.
- the full name allows to get data : someName.someList[1] // returns an int;
- the full name allows to set data : someName.someList[1] = 12;
Some words are identified as questions or actions.
// on Lists
root.show.videos.length
root.show.videos.clear()
root.show.videos.isEmpty
root.show.videos.isNotEmpty
root.show.videos.last
// on Maps
root.show.length
root.show.clear()
root.show.isEmpty
root.show.isNotEmpty
dynamic squad = MapListMap(); // create an empty map as squad.
squad.members = []; // add an empty list named 'members'
squad.activities = {}; // add also an empty map 'activities' at first level
dynamic squad = MapListMap({members: [], activities: {}); // the same in one line at construction
Note : With dot notation, create unknown data one level at a time (or use json).
// creation with direct structure
root = MapListMap({"dico":{"hello":{"US": "Hi", "FR": "bonjour"} }});
// If you plan to use heterogeneous data : better to precise type:
root = MapListMap( {"dico":<String,dynamic>{"hello":{"US": "Hi", "FR": "bonjour"} }});
// or use json string message that do the job with its internal types:
root = MapListMap(' {"dico":{"hello":{"US": "Hi", "FR": "bonjour"} }} ');
// follow previous sample : create with more complex data
root.dico.numbers = {"US": ["zero","one","two","three","four","five","sic","seven","eight","nine"]};
var numbers = root.dico.numbers;
var USnumbers = numbers.US;
print(USnumbers[3]);
Note : except for the root which must be declared dynamic, you can leave var at lower levels as the returned class is a MapList.
dynamic car = MapListMap(); // use dynamic to allow dot notation on root
car.brand = "Ford";
car.colors = ["blue","black","white"];
car.chosenColor = "white";
// now add this car to a larger set
dynamic myStuff = MapListMap();
myStuff.myCar = car; // create a property myCar with given values
dynamic list = MapListList();
list.add(15);
list.addAll([1, 2, 3]);
list.add({"date":"october 16"});
print(list); //[15, 1, 2, 3, {date: october 16}]
dynamic car = MapListMap();
car.name = "ford";
// add to this map several key:value in one shot or change existing
car.addAll({ "name":"Ford","price": 5000, "fuel":"diesel","hybrid":false});
store.wrongName.someData
To avoid the error "NoSuchMethodError: The getter 'someData' was called on null",
Dart takes care of the nullable notation.
The following code will return null or the data, without throwing error.
store.wrongName?.someData
store.eval('wrongName?.someData');
note: The interpreter takes care of the null notation.
- unknown key in a Map
- wrong index on a List
- misused of types , like indexing a Map or using key on a List
- for interpreter :
- wrong syntax
- malformed json
In all cases, MapList will returns null and logs a Warning on the standard logger.
MapList logs a Warning with a reminder of the initial error :
print(store.book[400]); // -> null
// WARNING: unexisting book [400] : null returned .
// Original message : RangeError (index): Invalid value: Not in range 0..3, inclusive: 4
You can protect downstream errors with a nullable option :
store.book[400]?.author = "zaza" ;
If the list doesn't exist at all, the nullable must be checked before the index to avoid error on the operator [ ]:
store.pocketBookList?[0].author = "zaza";
Dart allows this syntax recently with Dart 2.9.2.
Before 2.9.2 you cannot compile with a nullable before [0] in code.
The interpreter already allows this syntax.
MapList works on a basis of Map<String,dynamic> and List<dynamic> .
Using and adding json data, which are Map<dynamic,dynamic> and List<dynamic>, is full compliant.
This codes with a List will fail:
root.data = [11, 12, 13]; // will infer a List<int>
root.data.add(14); // ok
root.data.add("hello"); // will fail :type 'String' is not a subtype of type 'int'
If a type is not indicated, Dart infers the type from the current assignment and [11,12,13]
will be a List<int>
.
From there, you can only add other <int>
without errors, nothing else like "hello" without a crash.
Similar things can happen with a map.
This code will fail :
root.results = {"elapsed_time": 30, "temperature": 18} // is ok but result is a List of Map<String, int>
root.results.time = "12:58:00"; // will fail : type 'String' ("12:58:00") is not a subtype of type 'int' of 'value'
Think about adding type to the inline structures : These new codes will not fail:
root.data = <dynamic> [11, 12, 13];
root.data.add(14); // ok
root.data.add("hello"); // ok
root.results = <String,dynamic>{"elapsed_time": 60, "temperature": 40};
root.results.time = "12:58:00"; // now ok !
If you use constructors with a String structure, or 'dart json', MapList do the job of enlarging types to dynamic.
MapList try to avoid runtime errors:
If something is wrong, it logs Warning but continue without errors:
- On a wrong get, it logs message and returns null .
- On a wrong set : it logs message and do nothing else .
(To see the messages, you must set a logging listener in your code (@see standard logging package).)
aList["toto"]="riri";
**WARNING** index ["toto"] must be applied to a map. no action done.
aMap[0]="lulu":
**WARNING** [0] must be applied to a List. no action done.
print(aList.price);
**WARNING** Naming error: trying to get a key "price" in a List. null returned
Wrong index in List
print(root[200]);
**WARNING**: unknown accessor: . [200] : null returned .(followed by original dart error 'Not in range..')
Wrong json data in a String at runtime (if direct inline code, compiler will warn )
dynamic root = MapList('{"this": is not a valid entry }');
**WARNING** wrong json data. null returned .
(followed by original conversion error)
Mainly Type mismatch if inline data are not correctly casted .
Forgotten nullable in the evaluated path.
Leaving dart inline tolerance in script or json : comma at the end [11,12,]
I do prefer coding in yaml rather in json, but this have some defaults :
var yamlStructure = loadYaml(yamlString);
dynamic root = MapList(yamlStructure);
print(root.show.name); // ok
root.show.name = "new video title";
-> 'runtime Error: Unsupported operation: Cannot modify an unmodifiable Map';
If all get can work, no set are allowed because the standards yamlMap and yamlList are read only.
The most simple way to transform a read-only yaml in a full compliant json is the following :
root = MapList(json.decode(json.encode(yamlStructure)));
A MapList has a .json accessor to get the wrapped one if necessary.
MapList works with pointers, not copies :
json data stay synchronised between :
- direct access to json
- use with MapList in code
- or use of MapList interpreter.
As is, MapListMap and MapListList are in the category of DataObjects
You can mix free dynamic set of data and classical methods within a class that extends a MapList flavour.
Such a class is in an example with the following class Person :
class Person extends MapListMap{
Person(some):super(some);
}
As properties are free and in a json , a method cannot use this.someProperty as someProperty is not declared in class.
To allow retrieval of properties with dot notation, just use the keyword me. ( which is a cast of this as dynamic ).
In examples, we define an internal getter to the class Person, using dynamic data with me. :
int get age {
return (DateTime.now().year - me.birthDate.year);
}
An interpreter have some well known use cases :
- applying create or update on a data set from textual messages
- free interaction with data not known at compile time (knowledge base, blackboard pattern,...)
- using maps and lists as an open graph
You don't really need to use JsonNode as such, but MapList uses it to walk the graph.
JsonNode is a kind of canoe that navigates on the data graph with :
- fromNode
- edge
- toNode
- (ascript : the path or the remaining path)
When you create a JsonNode with a path, it returns the last step of its journey.
To view itn the toString returns (type) fromNode -- last edge in use---> (type) toNode
(As a node could be a large thing, toString returns the beginning... of the data )
Below is shown the internal structure to see internal data.
var aJson = [ [1, 2, 3], [11, 12], {"A": "AA", "B": "BB"}, "andSoOn" ];
print(JsonNode(aJson, '[0][1]')); // (list)[1, 2, 3] --- 1 -> (int) 2
print(JsonNode(aJson, '[2]["B"]')); // (map){A: AA, B: BB} --- B -> (String) BB
print(JsonNode(aJson, '[2].length')); // (map){A: AA, B: BB} --- length -> (int) 2
var aJson = [ [1, 2, 3], [11, 12], {"A": "AA", "B": "BB"}, "andSoOn" ];
print(JsonNode(aJson,'[0]')); // (list)[[1, 2, 3], [11... --- 0 -> (list) [1, 2, 3]
print(JsonNode(aJson,'[2]')); //(list)[[1, 2, 3], [11... --- 2 -> (map) {A: AA, B: BB}
If you plan to use directly JsonNode, you can get the data by .value
(which is a convenient name to get the last toNode )
assert(JsonNode(aJson, '[2].B').value == "BB");
JsonNode recognize some keywords:
- .length
- .isEmpty
- .isNotEmpty
- .last (on Lists )
- .clear()
Note about length
Always use .length to get the length of a List or a Map. If there is a key length in a map, you can reach it with notation ["length"]
dynamic store = MapList('{"bikes":[{"name":"Fonlupt", "length":2.1, "color" : "green" }]}');
assert(store.eval('bikes[0].length') == 3);
assert(store.eval('bikes[0]["length"]')== 2.1);
When a path ends with an unknown name in a map, the last node is null but not the trip :
print(JsonNode(aJson, 'questions.newData'));// (map){A: AA, B: BB} --- newData -> (Null) null
A caller, like MapList do, can check the results and set the data with fromNode[edge] .
(if the path starts at the very beginning, the fromNode is also null and the edge must be applied to the root )
Same precautions has to be taken on datatypes to avoid bad surprises.
Writing inline data with types could be cumbersome :
var aJson = <dynamic>[ [1, 2, 3], <String,dynamic> {"A": "AA", "B": "BB"}, "andSoOn" ];
Tip: Prefer using a json String and let MapList do a json.decode(string) do the job.
- type in your inline structure to see its correctness :
[ [1, 2, 3], {"A": "AA", "B": "BB"}, "andSoOn" ]
- wrap it in quotes :
'[ [1, 2, 3], {"A": "AA", "B": "BB"}, "andSoOn" ]'
- use it in MapList(someString)
MapList uses underline the previous JsonNode mechanism, get the value and allows to create and set data.
You can use same notations than in code to access a data, classical or dot notation.
MapList will return directly the data:
- for an end leaf, it returns the value,
- for an intermediary node, it returns the json wrapped in a new MapList (returning a MapList allows to combine interpreter and direct code with dot notation in code.)
Notice that the root is the executor and is not repeated in the path :
The script below returns a simple data :
dynamic book = MapList('{ "name":"zaza", "friends": [{"name": "lulu" }]}');
print(book.eval('friends[0].name')); // --> lulu);
All notation styles are allowed :
root.eval('["show"]["videos"][1]["questions"][1]["name"]') // classical notation
root.eval('show.videos[1].questions[1].options[2].answer') // dot notation
Special word returns also direct values:
if (store.eval('store.bikes.isEmpty')) print ('what a disaster');
MapList interpreter takes care of an assignment in the script.
The Left Hand Side of a script with assignment is the path to get by MapList.
The Right Hand Side is evaluated as a simple type data or as a json structure.
dynamic squad = MapList(); // will create a default Map
squad.eval('name = "Super hero squad"'); // add a String data
squad.eval('homeTown = "Metro City"'); // another
squad.eval('formed = 2016'); // add an int
squad.eval('active = true'); // add a bool
squad.eval('score = 38.5'); // add a double
squad.eval('overhauls = ["2008/04/10", "2102/05/01", "2016/04/17"]'); // add structured data
- add(something)
- Only for Lists : add a new element
- addAll(several something)
- add all elements of a map to a map
- all elements of a list to a list.
- remove(something)
- Remove an entry of a map
- Remove an element in a list
- length = <int>
- force the length of a List
See the previous chapter on errors and logs for common access errors.
Some errors normally detected by compiler can happen in interpreted string.
As an example below is a missing parenthesis around functions.
root.eval('clear'); // WARNING cannot search a key (clear) in a List<dynamic>
root.eval('clear()'); // ok
Interpreter takes care of nullable notations at all levels :
store.eval('wrongName?.someData')
store.eval("book[4]?.author
store.eval("bookList?[0]") // Even if Dart is not in 9.2, the interpreter allows this nullable.
store.eval("bookList?[0]?.date")
Probably some in the analysis of syntax in interpreter : in case of trouble verify deeply your strings.
MapList uses Symbol without mirrors and get the symbol name by hand : This could have issues with dart.js minifier, this has not been tested here.