A Delphi / Lazarus / C++Builder simple and small class for fast JSON parsing.
Some points of interest:
- Simple Object-Pascal native code using TList as internal data structure.
- Single-pass string parser.
- Compatible (aimed):
- Delphi 7 up to now.
- Lazarus.
- C++Builder 2006 up to now.
- Tested with:
- BDS 2006 (Delphi and BCP)
- Lazarus 2.3.0 (FPC 3.2.2)
- C++Builder XE2 and 10.2.
- Just one unit (
McJSON
), just one class(TMcJsonItem
). - Inspired by badunius/myJSON.
- Improved parser after applying Tan Li Hau's article.
- Performance tests using C++Builder and comparing:
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 \test\test99.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 false
The 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, index
And 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 key
Using 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.AsString
Change 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('\dir\subdir') + '}';
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: \b\t\n\f\r\\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: \b\t\n\f\r\u\"\\"]}
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: \b\t\n\f\r\uFFFF\"\\"]
value; string; Key=; Value=#1 UTF8 example: motivação; JSON="#1 UTF8 example: motivação"
value; string; Key=; Value=#2 Scapes: \b\t\n\f\r\uFFFF\"\\; JSON="#2 Scapes: \b\t\n\f\r\uFFFF\"\\"
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:
- As
TMcJsonItem
objects are instantiated in hierarchical structure using listsfChild
, there is a problem to create fields that propagate automatically between items. A solution under study tries to create a new parent classTMcJson
which objects will be like roots and haveTMcJsonItem
objects as its children. - Trying to follow and confirm the specification using JSONLint.
A performance test have been done with the original myJSON
, LkJson
, JsonTools
and uJSON
units.
Here is a summary of the tests.
- Generate a JSON with 50k items like:
{... {"keyi":"valuei"}... }
- Save to file.
- Parse from memory (copy object forcing a parse).
- Load from file (and parsing).
- Access 1k items randomly.
And about the compiler and machine used:
- C++Builder VCL examples built with BDS 2006 (the older version I have).
- Very old 32 bits machine: Intel Core 2 CPU T5500 1.66GHz 4 GB RAM.
The next table summarizes the results1:
Library | Generate | Save | Parse | Load | Access | Total |
---|---|---|---|---|---|---|
McJSON 2 |
.11 s | .07 s | .12 s | .09 s | .83 s | 1.25 s |
LkJson 2 |
.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 |
- Good performance, but not the better about random access due to the use of TList.
- Simple and smart interface using "AsXXX" getters and setters (not invented here).
- Generate using:
Json->Add("key")->AsString = "value"
. - Parse using:
JsonP->AsJSON = Json->AsJSON
.
- Good performance generating and parsing and even better with random access due to "Balanced Search Tree"
TlkBalTree
. - TLkJSONBase and other derivated classes force to cast objects using the "as" operator. In C++Builder, this requires
dynamic_cast
making the code verbosy. - Generate using:
Json->Add("key", "value")
. - Parse using:
JsonP = dynamic_cast<TlkJSONObject*>(TlkJSON::ParseText(NULL, TlkJSON::GenerateText(NULL, Json)))
.
- Very nice and interesting code focused on the concept of Tokens.
- Also uses TList as internal data structure.
- It needs a performance review.
- Generate using:
Json->Add("key", "value")
. - Parse using:
JsonP->Value = Json->AsJson
.
- Performance deteriored due the recurrent use of wsTrim().
- Generate using:
Json->Item["key"]->setStr("value")
. - Parse using:
JsonP->Code = Json->getJSON()
.
- Less verbosy in C++ than
LkJson
, but the colection of classes also will force casting withdynamic_cast
. - Uses TStringList as a "Hash Map" [string] -> [object address]. The quotation marks here is because I think the string entry is not a true hash within TStringList.
- In some aspects, the methods interface might became puzzling.
- It needs a performance review.
- With
uJSON
, there seems to be a performance problem related totoString()
. - This unit is used in other projects, e.g. Diffbot API Delphi Client Library (same author).
- Generate using:
Json->put("key", "value")
. - Parse using:
JsonP = new TJSONObject(Json->toString())
. SaveToFile
doesn't exist, so it has usedTStringList->SaveToFile()
after fillingText
withJson->toString()
.