A Delphi / Lazarus / C++Builder simple and small class for fast JSON parsing.
Some points of interest:
McJSON), just one class(TMcJsonItem).uses
McJSON;
...
function Test99(out Msg: string): Boolean;
var
Json: TMcJsonItem;
i: Integer;
begin
Msg := 'Test: Github readme.md content';
Json := TMcJsonItem.Create();
try
try
// add some pairs.
Json.Add('key1').AsInteger := 1;
Json.Add('key2').AsBoolean := True;
Json.Add('key3').AsNumber := 1.234;
Json.Add('key4').AsString := 'value 1';
// add an array
Json.Add('array', jitArray);
for i := 1 to 3 do
Json['array'].Add.AsInteger := i;
// save a backup to file
if (Json['array'].Count = 3) then
Json.SaveToFile('test99.json');
// remove an item
Json.Delete('array');
// oops, load the backup
if (Json.Count = 4) then
Json.LoadFromFile('test99.json');
// test final result
Result := (Json.AsJSON = '{"key1":1,"key2":true,"key3":1.234,"key4":"value 1","array":[1,2,3]}');
except
Result := False;
end;
finally
Json.Free;
end;
end;Will produce testtest99.json:
{
"key1": 1,
"key2": true,
"key3": 1.234,
"key4": "value 1",
"array": [
1,
2,
3
]
}#include "McJson.hpp"
...
bool Test99(AnsiString& Msg)
{
bool Result;
TMcJsonItem* Json = NULL;
Msg = "Test: Github readme.md content";
Json = new TMcJsonItem();
try
{
try
{ // add some pairs.
Json->Add("key1")->AsInteger = 1;
Json->Add("key2")->AsBoolean = true;
Json->Add("key3")->AsNumber = 1.234;
Json->Add("key4")->AsString = "value 1";
// add an array
Json->Add("array", jitArray);
for (int i = 1; i <= 3 ; i++)
Json->Values["array"]->Add()->AsInteger = i;
// save a backup to file
if (Json->Values["array"]->Count == 3)
Json->SaveToFile("test99.json");
// remove an item
Json->Delete("array");
// oops, load the backup
if (Json->Count == 4)
Json->LoadFromFile("test99.json");
// test final result
Result = (Json->AsJSON ==
"{"key1":1,"key2":true,"key3":1.234,"key4":"value 1","array":[1,2,3]}");
}
catch(...)
{
Result = false;
}
}
__finally
{
if (Json) delete (Json);
}
return (Result);
}Please considere read Unit Tests in test folder for a complete list of McJSON use cases.
Just use the AsJSON property
var
N: TMcJsonItem;
begin
N := TMcJsonItem.Create;
N.AsJSON := '{"i": 123, "f": 123.456, "s": "abc", "b": true, "n": null}';
// use N here
N.Free;
end; If you want to check if a JSON string is valid:
Answer := N.Check( '{"i":[123}' ); // Answer will be falseThe Check method will not raise any exception. The example above will catch and hide the Error while parsing text: "expected , got }" at pos "10" exception.
If you need to catch and manage exceptions, use CheckException like:
try
Answer := N.CheckException( '{"k":1, "k":2}' ); // Answer will be false
except
on E: Exception do
begin
// Error while parsing text: "duplicated key k" at pos "11"
end;
end; McJSON allows a simple way to access items through paths. We can use '/', '' or '.' as path separators.
N.AsJSON := '{"o": {"k1":"v1", "k2":"v2"}}';
// access and change second object's value
N.Path('o.k2').AsString := 'value2';Results in:
{
"o": {
"k1":"v1",
"k2":"value2"
}
}Note that Path() does not accept indexes yet, like this:
N.AsJSON := '{"o": [{"k1":"v1"}, {"k2":"v2"}]';
N.Path('o[1].k2').AsString := 'value2';Since version 1.0.4 McJSON allows to use property shorteners like in Andreas Hausladen's Json Data Objects.
// access (automatic creation as in JDO)
Obj.S['foo'] := 'bar';
Obj.S['bar'] := 'foo';
// array creation, Obj is the owner of 'array'
Obj.A['array'].Add.AsInteger := 10;
Obj.A['array'].Add.AsInteger := 20;
// object creation, 'array' is the owner of ChildObj
ChildObj := Obj['array'].Add(jitObject);
ChildObj.D['value'] := 12.3;
// array creation, ChildObj is the owner of 'subarray'
ChildObj.A['subarray'].Add.AsInteger := 100;
ChildObj.A['subarray'].Add.AsInteger := 200;Results in:
{
"foo":"bar",
"bar":"foo",
"array":[
10,
20,
{
"value":12.3,
"subarray":[
100,
200
]
}
]
}Here is how to access all items (children) of a JSON object and change their value type and content.
N.AsJSON := '{"o": {"k1":"v1", "k2":"v2"}}';
// type and value: from string to integer
for i := 0 to N['o'].Count-1 do
N['o'].Items[i].AsInteger := i+1; Results in:
{
"o": {
"k1":1,
"k2":2
}
}We can use the Items[index] and Values['key'] properties to access items inside objects and arrays.
Since version 0.9.5, we can use the At(index, 'key') or At('key', index) as shorteners.
N.AsJSON := '{"a": [{"k1":1,"k2":2},{"k1":10,"k2":20}]}';
// how to access k2 of second object.
i := N['a'].Items[1].Values['k2'].AsInteger; // i will be equal to 20
i := N['a'].Items[1]['k2'].AsInteger; // uses the Values[] as default property
i := N['a'].At(1, 'k2').AsInteger; // shortener: index, key
i := N.At('a', 1)['k2'].AsInteger; // shortener: key, indexAnd there are other uses without the key parameter:
N.AsJSON := '{"k1":1,"k2":2,"k3":3,"k4":4}';
i := N.Items[2].AsInteger; // i will be equal to 3
i := N.At(2).AsInteger; // shortener: just index
i := N.At('k3').AsInteger; // shortener: just keyUsing Delphi enumerator you can browse item's object children and values.
var
N, item: TMcJsonItem;
begin
N := TMcJsonItem.Create;
N.AsJSON := '{"o": {"k1":"v1", "k2":"v2"}}';
for item in N['o'] do
// use item here, e.g. item.Key, item.Value, item.AsStringChange all values of an object with multiple items. Not so common out there.
N.AsJSON := '{"o": {"k1":"v1", "k2":"v2"}}';
N['o'].AsString := 'str';Results in:
{
"o": {
"k1": "str",
"k2": "str"
}
}And if it is necessary to change the type of o:
N['o'].ItemType := jitValue;
N['o'].AsString := 'str';Results in:
{
"o": "str"
}Convert from array to object type and vice-versa. Also, not so common out there.
N.AsJSON := '{ "k1": ["1", "2"], "k2": {"1": "a", "2": "b"} }';
N['k1'].ItemType := jitObject; // convert array to object with items
N['k2'].ItemType := jitArray ; // convert object with items to array Results in:
{
"k1": {
"0": "1",
"1": "2"
},
"k2": [
"a",
"b"
]
}Insert some items using keys and position.
P.Insert('c', 0).AsInteger := 3;
P.Insert('b', 0).AsInteger := 2;
P.Insert('a', 0).AsInteger := 1;Results in:
{
"a": 1,
"b": 2,
"c": 3
}Also, it is possible to insert objects in arrays.
Q.AsJSON := '{"x":0}';
P.ItemType := jitArray;
P.Insert(Q, 1);Results in:
[
1,
{
"x": 0
},
2,
3
]Important: since version 0.9.3, Add() and Insert() will clone arguments of type TMcJsonItem. So, we have to free memory for Q too:
P.Free;
Q.Free;Since version 1.0.5 strings can be escaped with McJsonEscapeString() helper function:
N.AsJSON := '{"path": ' + McJsonEscapeString('dirsubdir') + '}'; Results in:
{
"path": "\dir\subdir"
}In version 1.0.6 was introduced the TJEscapeType enum used in McJsonEscapeString() with these escape levels:
jetNormal : escapes #8 #9 #10 #12 #13 " .jetStrict : Normal + /.jetUnicode : Strict + uXXXX.jetNone : backwards compatibility.These levels are inspired by Lazarus' helper function StringToJSONString() from library fpjson.
Let's see how to inspect all the inner data structure, types and values of a TMcJsonItem object.
//---------------------------------------------------------------------------
void
TFormMain::Inspect(TMcJsonItem* AMcJItem, AnsiString Ident)
{
if (!AMcJItem) return;
// log current
MyLog( Ident + ItemToStr(AMcJItem) );
// log child
if ( AMcJItem->HasChild )
{
Ident = " " + Ident;
for (int i=0; i < AMcJItem->Count; i++)
{ // use Value not Child because are note using Key[].
Inspect( AMcJItem->Items[i], Ident );
}
}
}
//---------------------------------------------------------------------------
String
TFormMain::ItemToStr(TMcJsonItem* AMcJItem) const
{
String Ans = "";
if (AMcJItem)
Ans = AMcJItem->GetTypeStr() +
"; " + AMcJItem->GetValueStr() +
"; Key=" + AMcJItem->Key +
"; Value="+ AMcJItem->Value +
"; JSON=" + AMcJItem->AsJSON;
return (Ans);
}
//---------------------------------------------------------------------------And using a example like testInspect.json:
{
"foo": "bar",
"array": [
100,
20
],
"arrayObj": [
{
"key1": 1.0
},
{
"key2": 2.0
}
],
"Msg": [
"#1 UTF8 example: motivação",
"#2 Scapes: btnfr\uFFFF"\"
]
}Calling Inspect() with a Json object loaded with testInspect.json:
TMcJsonItem* Json = new TMcJsonItem();
if (Json)
{
Json->LoadFromFile("testInspect.json");
Inspect(Json);
delete (Json);
}Results in:
object; string; Key=; Value=; JSON={"foo":"bar","array":[100,20],"arrayObj":[{"key1":1.0},{"key2":2.0}],"Msg":["#1 UTF8 example: motivação","#2 Scapes: btnfru"\"]}
value; string; Key=foo; Value=bar; JSON="foo":"bar"
array; string; Key=array; Value=; JSON="array":[100,20]
value; number; Key=; Value=100; JSON=100
value; number; Key=; Value=20; JSON=20
array; string; Key=arrayObj; Value=; JSON="arrayObj":[{"key1":1.0},{"key2":2.0}]
object; string; Key=; Value=; JSON={"key1":1.0}
value; number; Key=key1; Value=1.0; JSON="key1":1.0
object; string; Key=; Value=; JSON={"key2":2.0}
value; number; Key=key2; Value=2.0; JSON="key2":2.0
array; string; Key=Msg; Value=; JSON="Msg":["#1 UTF8 example: motivação","#2 Scapes: btnfruFFFF"\"]
value; string; Key=; Value=#1 UTF8 example: motivação; JSON="#1 UTF8 example: motivação"
value; string; Key=; Value=#2 Scapes: btnfruFFFF"\; JSON="#2 Scapes: btnfruFFFF"\"
Since version 0.9.0, empty keys will be parsed and checked withou errors:
N.AsJSON := '{"": "value"}';And ToString() will produce a valid JSON object:
{
"": "value"
}Internally, it will use the C_EMPTY_KEY constant string as content of the fKey field.
Since version 0.9.2, strings with not escaped line breakes will be parsed with errors:
N.AsJSON := '{"key": "value' + #13 + '"}';Will raise exception:
Error while parsing text: "line break" at pos "14"
McJSON can load from ASCII and UTF-8 files (with or without BOM). See LoadFromFile method.
The SaveToFile method will write using UTF-8 encoding.
Note: since vertion 1.0.4, the test project's source code in Lazarus was converted to UTF-8, so the asUTF8 parameter was set to false.
The world is not perfect and neither am I. Here are some known issues:
TMcJsonItem objects are instantiated in hierarchical structure using lists fChild, there is a problem to create fields that propagate automatically between items. A solution under study tries to create a new parent class TMcJson which objects will be like roots and have TMcJsonItem objects as its children.A performance test have been done with the original myJSON, LkJson, JsonTools and uJSON units.
Here is a summary of the tests.
{... {"keyi":"valuei"}... }And about the compiler and machine used:
The next table summarizes the results1:
| Library | Generate | Save | Parse | Load | Access | Total |
|---|---|---|---|---|---|---|
McJSON2 |
.11 s | .07 s | .12 s | .09 s | .83 s | 1.25 s |
LkJson2 |
.30 s | .11 s | .47 s | .36 s | .01 s | 1.24 s |
JsonTools |
48.00 s | .70 s | 39.00 s | 40.00 s | .48 s | 1.2 min |
myJSON |
50.00 s | .07 s | 5.1 min | 7.7 min | 1.60 s | 13.1 min |
uJSON |
18.6 min | 20.1 min | 17.5 min | 4.31 s | 53.02 s | 57.6 min |
McJSONJson->Add("key")->AsString = "value".JsonP->AsJSON = Json->AsJSON.LkJsonTlkBalTree.dynamic_cast making the code verbosy.Json->Add("key", "value").JsonP = dynamic_cast<TlkJSONObject*>(TlkJSON::ParseText(NULL, TlkJSON::GenerateText(NULL, Json))).JsonToolsJson->Add("key", "value").JsonP->Value = Json->AsJson.myJSONJson->Item["key"]->setStr("value").JsonP->Code = Json->getJSON().uJSONLkJson, but the colection of classes also will force casting with dynamic_cast.uJSON, there seems to be a performance problem related to toString().Json->put("key", "value").JsonP = new TJSONObject(Json->toString()).SaveToFile doesn't exist, so it has used TStringList->SaveToFile() after filling Text with Json->toString().Metric: average time in seconds (s) for 5 consecutive executions. Total is the average of partial tests. Some results converted to minutes (min). ↩
Version 1.0.5. Improved Test JSON 0.9.0 project that will be released soon. ↩ ↩2