Extend TMS WEB Core with JS Libraries with Andrew: Epic JSON Primer (part 2)
20 : Relative Performance.
I’ve often heard that while it is possible to use the TJSONObject variant of these approaches (the PAS code we’ve been working so diligently on), it is preferable instead to use the WC variant as it will be better performing. We’ll put that to the test here in a bit of a contrived example. Often, readability and re-usability of code is more important than straight-up performance, particularly for difficult code that isn’t executed frequently. But at the same time, situations certainly do come up where a bit of code is executed frequently in a tight loop, and squeezing out every bit of performance is important.
procedure TForm1.WebButton1Click(Sender: TObject); var WC_Object: TJSObject; PAS_Object: TJSONObject; ElapsedTime: TDateTime; i: Integer; Count: Integer; begin ElapsedTime := Now; // JS Create 1,000,000 Objects asm var JS_Object = {}; for (var i = 0; i <= 999999; i++) { JS_Object['test '+i] = 'test '+i; } end; console.log('JS Create: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms'); ElapsedTime := Now; // JS Count Objects asm Count = Object.keys(JS_Object).length; end; console.log('JS Count: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms'); ElapsedTime := Now; // WC Create 1,000,000 Objects WC_Object := TJSObject.new; for i := 0 to 999999 do WC_Object['test '+IntToStr(i)] := 'test '+IntToStr(i); console.log('WC Create: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms'); ElapsedTime := Now; // WC Count Objects Count := length(TJSObject.keys(WC_Object)); console.log('WC Count: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms'); ElapsedTime := Now; // PAS Create 10,000 Objects PAS_Object := TJSONObject.Create; for i := 0 to 9999 do PAS_Object.AddPair('test '+IntToStr(i),'test '+IntToStr(i)); console.log('PAS Create: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms'); ElapsedTime := Now; // PAS Count Objects Count := PAS_Object.Count; console.log('PAS Count: '+IntToStr(MillisecondsBetween(Now,ElapsedTime))+' ms'); end; console.log output: JS Create: 1154 ms JS Count: 301 ms WC Create: 1006 ms WC Count: 272 ms PAS Create: 27439 ms PAS Count: 0 ms
So that was eye-opening! I had to actually reduce the PAS loop to be a mere 10,000 Objects instead of the 1,000,000 Objects of the JS and WC loops just so that it would complete. And it still took nearly 30x longer to create the JSON Objects (so extrapolating, it is 3,000 times slower?!) Seems that someone somewhere has some optimization work to do. Note that the count is trivial in the PAS case so it doesn't really even register, where as with JS and WC it actually has to do quite a bit of work to get at that number. Takeaway? Well, same as before, really, just surprising that the penalty is so high. Hopefully that situation will improve in time. Or if it doesn't, that's a pretty solid argument for migrating PAS code that is in any kind of loop to use the JS or WC variant. To be fair, this is likely the only code in this entire article where any performance difference would even be detectable.
Also note that, running the test over and over again, the JS and WC Create times vary quite a bit, sometimes down to half as long. But the Count times didn't fluctuate much at all. PAS remained largely the same. This could be due to many things, but there's a lot of JS optimization that happens all over the place, so not too surprising. It does make it a challenge to come up with reasonable benchmarks though - you need to run lots of tests to get an average figure for it to be meaningful, and even then you have to factor in the conditions surrounding the test really carefully - was the app already loaded, was there an animation running somewhere, etc.
21 : Iterate through JSON Object Elements.
We've had examples of this kind of thing here and there already, but here's a more explicit example for all three variations.
procedure TForm1.WebButton1Click(Sender: TObject); var WC_Object: TJSObject; PAS_Object: TJSONObject; i: Integer; const SampleObjectData = '{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"}'; begin asm var JS_Object = JSON.parse(SampleObjectData); end; WC_Object := TJSJSON.parseObject(SampleObjectData); PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) as TJSONObject; asm for (var Key in JS_Object) { console.log('JS Key: '+Key+' Value: '+JS_Object[Key]) } end; i := 0; while (i < length(TJSOBject.keys(WC_Object))) do begin console.log('WC Key: '+string(TJSObject.keys(WC_Object)[i])+' Value: '+string(WC_Object[TJSObject.keys(WC_Object)[i]])); i := i + 1; end; i := 0; while (i < PAS_Object.Count) do begin console.log('PAS Key: '+PAS_Object.Pairs[i].JSONString.Value+' Value: '+PAS_Object.Pairs[i].JSONValue.Value); i := i + 1; end; end; console.log output: JS Key: apple Value: fruit JS Key: banana Value: fruit JS Key: orange Value: fruit JS Key: carrot Value: vegetable JS Key: potato Value: vegetable WC Key: apple Value: fruit WC Key: banana Value: fruit WC Key: orange Value: fruit WC Key: carrot Value: vegetable WC Key: potato Value: vegetable PAS Key: apple Value: fruit PAS Key: banana Value: fruit PAS Key: orange Value: fruit PAS Key: carrot Value: vegetable PAS Key: potato Value: vegetable
22 : Traverse Entire JSON Object.
We now have everything we need to tackle any kind of JSON that comes our way. Typically there isn't any need to traverse an entire JSON Object directly. Normally you're just looking to pick out certain values or update others. One use case though is to output a formatted version of the JSON Object. There are lots of online resources for doing this kind of thing for you - just copy and paste the JSON you'd like formatted, and it will output a nicer (pretty or beautified) version easily enough. Here we do something similar, annotating whatever we find. Note that the output is not at all JSON, just a list of what is in it and details about the data types. However, it should be able to regurgitate whatever JSON you through at it. It isn't pretty, but should cover everything.
procedure TForm1.WebButton1Click(Sender: TObject); var WC_Object: TJSObject; PAS_Object: TJSONObject; const SampleObjectData = '[[[123,{"key":true,"another key":"abc","more keys":[1,2,false]},null]]]'; function WC_Print(Element:TJSObject; Indentation:String; Key:String):String; var str: String; typ: String; begin str := ''; typ := ''; if (Element = nil) then begin str := 'Null (Null)'; typ := 'Null'; end else if (Element.ToString = 'true') then begin str := 'True (Boolean)'; typ := 'Boolean'; end else if (Element.ToString = 'false') then begin str := 'False (Boolean)'; typ := 'Boolean'; end else if (TJSJSON.stringify(Element).startsWith('"')) then begin str := Element.toString+' (String)'; typ := 'String'; end else if (TJSJSON.stringify(Element).startsWith('{')) then begin str := 'Object {'; typ := 'Object'; end else if (TJSJSON.stringify(Element).startsWith('[')) then begin str := 'Array ['; typ := 'Object'; end else begin str := Element.ToString+' (Number)'; typ := 'Number'; end; if (Key = '') then console.log(Indentation+' '+str) else console.log(Indentation+' '+Key+': '+str); Result := typ; end; procedure WC_Output(JSONObject:TJSObject; Indentation:String); var i: integer; typ: string; const indent = '____'; begin if (TJSJSON.stringify(JSONObject).startsWith('[')) then begin if (Indentation = '') then begin console.log('WC ['); Indentation := indent; end; for i := 0 to (length(TJSObject.keys(JSONObject))-1) do begin typ := WC_Print(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation, IntToStr(i)); if (typ = 'Object') then begin WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent); console.log(Indentation+' }'); end else if (typ = 'Array') then begin WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent); console.log(Indentation+' ]'); end; end; if (Indentation = indent) then console.log(' ]'); end else if (TJSJSON.stringify(JSONObject).startsWith('{')) then begin if (Indentation = '') then begin console.log('WC {'); Indentation := '____'; end; for i := 0 to (length(TJSObject.keys(JSONObject))-1) do begin typ := WC_Print(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation, string(TJSObject.keys(JSONObject)[i])); if (typ = 'Object') then begin WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent); console.log(Indentation+' }'); end else if (typ = 'Array') then begin WC_Output(TJSObject(JSONObject[String(TJSObject.keys(JSONObject)[i])]), Indentation+indent); console.log(Indentation+' ]'); end; end; if (Indentation = indent) then console.log('}'); end else begin WC_Print(JSONObject,Indentation,'') end; end; function PAS_Print(Element:TJSONValue; Indentation:String; Key:String):String; var str: String; typ: String; begin str := ''; typ := ''; if (Element.ClassName = 'TJSONNull') then begin str := 'Null (Null)'; typ := 'Null'; end else if (Element.ClassName = 'TJSONTrue') then begin str := 'True (Boolean)'; typ := 'Boolean'; end else if (Element.ClassName = 'TJSONFalse') then begin str := 'False (Boolean)'; typ := 'Boolean'; end else if (Element.ClassName = 'TJSONString') then begin str := Element.toString+' (String)'; typ := 'String'; end else if (Element.ClassName = 'TJSONNumber') then begin str := Element.toString+' (Number)'; typ := 'String'; end else if (Element.ClassName = 'TJSONObject') then begin str := 'Object {'; typ := 'Object'; end else if (Element.ClassName = 'TJSONArray') then begin str := 'Array ['; typ := 'Object'; end; if (Key = '') then console.log(Indentation+' '+str) else console.log(Indentation+' '+Key+': '+str); Result := typ; end; procedure PAS_Output(JSONObject:TJSONObject; Indentation:String); var i: integer; typ: string; const indent = '____'; begin if (JSONObject.ClassName = 'TJSONArray') then begin if (Indentation = '') then begin console.log('PAS ['); Indentation := indent; end; for i := 0 to ((JSONObject as TJSONArray).Count - 1) do begin typ := PAS_Print((JSONObject as TJSONArray)[i] as TJSONValue, Indentation, IntToStr(i)); if (typ = 'Object') then begin PAS_Output((JSONObject as TJSONArray)[i] as TJSONObject, Indentation+indent); console.log(Indentation+' }'); end else if (typ = 'Array') then begin PAS_Output((JSONObject as TJSONArray)[i] as TJSONArray, Indentation+indent); console.log(Indentation+' ]'); end; end; if (Indentation = indent) then console.log(']'); end else if (JSONObject.ClassName = 'TJSONObject') then begin if (Indentation = '') then begin console.log('PAS {'); Indentation := '____'; end; for i := 0 to (JSONObject.Count - 1) do begin typ := PAS_Print(JSONObject.Items[i], Indentation, JSONObject.Pairs[i].JSONString.Value); if (typ = 'Object') then begin PAS_Output(JSONObject.Items[i] as TJSONObject, Indentation+indent); console.log(Indentation+' }'); end else if (typ = 'Array') then begin PAS_Output(JSONObject.Items[i] as TJSONArray, Indentation+indent); console.log(Indentation+' ]'); end; end; if (Indentation = indent) then console.log('}'); end else begin PAS_Print(JSONObject,Indentation,'') end; end; begin asm var JS_Object = JSON.parse(SampleObjectData); end; WC_Object := TJSJSON.parseObject(SampleObjectData); PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) as TJSONObject; asm function JS_Print(Element, Indentation, Key) { var str = ''; var typ = ''; if (Element === null) { str = 'Null (Null)'; typ = 'Null'; } else if (Element === true) { str = 'True (Boolean)'; typ = 'Boolean'; } else if (Element === false) { str = 'False (Boolean)'; typ = 'Boolean'; } else if (Array.isArray(Element)) { str = 'Array ['; typ = 'Array'; } else if (typeof Element === 'string') { str = '"'+Element+'" (String)'; typ = 'String'; } else if (typeof Element === 'number') { str = Element+' (Number)'; typ = 'Number'; } else if (typeof Element === 'object') { str = 'Object {'; typ = 'Object'; } if (Key === undefined) { console.log(Indentation+' '+str); } else { console.log(Indentation+' '+Key+': '+str); } return(typ) } function JS_Output(JSONObject, Indentation) { const indent = '____'; if (Array.isArray(JSONObject)) { if (Indentation == '') { console.log("JS ["); Indentation = indent; } for (var i = 0; i < JSONObject.length; i++) { switch (JS_Print(JSONObject[i],Indentation,i)) { case 'Object': JS_Output(JSONObject[i],Indentation+indent); console.log(Indentation+" }"); break; case 'Array': JS_Output(JSONObject[i],Indentation+indent); console.log(Indentation+" ]"); break; } } if (Indentation == indent) { console.log("]") } } else if (typeof JSONObject === 'object') { if (Indentation == '') { console.log("JS {"); Indentation = '____'; } for (var key in JSONObject) { switch (JS_Print(JSONObject[key],Indentation,key)) { case 'Object': JS_Output(JSONObject[key],Indentation+indent); console.log(Indentation+" }"); break; case 'Array': JS_Output(JSONObject[key],Indentation+indent); console.log(Indentation+" ]"); break; } } if (Indentation == indent) { console.log("}") } } else { JS_Print(JSONObject,Indentation); } } end; asm JS_Output(JS_Object,''); end; WC_Output(WC_Object,''); PAS_Output(PAS_Object,''); end; console.log output (just JS, WC and PAS are the same): JS [ ____ 0: Array [ ________ 0: Array [ ____________ 0: 123 (Number) ____________ 1: Object { ________________ key: True (Boolean) ________________ another key: "abc" (String) ________________ more keys: Array [ ____________________ 0: 1 (Number) ____________________ 1: 2 (Number) ____________________ 2: False (Boolean) ________________ ] ____________ } ____________ 2: Null (Null) ________ ] ____ ] ]
23 : Compare JSON.
The quickest (or at least, the simplest) way to compare two JSON Objects would be to just convert them to strings and check if they match. But this may not be accurate, given the unordered nature of JSON Object elements. If all you have is an Array, or if you know your Object is sorted by Key or by some other deterministic method (you created the elements in a particular order consistently, for example) then comparing strings could still work. But if that's not the case, another approach is needed to determine this 'semantic' equivalence. Here we iterate through one of the JSON Objects and then check that we can find everything in the appropriate place in a second JSON Object. For the Objects, we just lookup the Keys so they should match and the order doesn't matter. For Arrays, we use the index and the order does indeed matter. This approach also makes it possible to adjust the strictness of the comparison. Like if you wanted to have a case-insensitive match on string Values, for example, or less accuracy (or more!) when it comes to comparing numbers.
procedure TForm1.WebButton1Click(Sender: TObject);
var
WC_Object1: TJSObject;
WC_Object2: TJSObject;
PAS_Object1: TJSONObject;
PAS_Object2: TJSONObject;
const
SampleObjectData1 = '[[[123,{"key":true,"another key":"abc","more keys":[1,3,false]},null,[{"a":"1","b":2,"c":"3"}]]]]';
SampleObjectData2 = '[[[123,{"another key":"abc","key":true,"more keys":[1,3,false]},null,[{"c":"3","b":2,"a":"1"}]]]]';
function WC_Compare(Element1: TJSObject; Element2:TJSObJect):Boolean;
var
test: Boolean;
i: integer;
begin
test := true;
if ((Element1 = nil) and (Element2 = nil)) then test := true
else if (Element1 = nil) or (Element2 = nil) then test := false
else if ((Element1.toString = 'true') and (Element2.toString = 'true')) then test := true
else if ((Element1.toString = 'false') and (Element2.toString = 'false')) then test := true
else if (TJSJSON.stringify(Element1).startsWith('[')) and (TJSJSON.stringify(Element2).startsWith('[')) and (length(TJSObject.keys(Element1)) = length(TJSObject.keys(Element2))) then
begin
for i := 0 to (length(TJSObject.keys(Element1))-1) do
begin
if not(WC_Compare(TJSObject(Element1[String(TJSObject.keys(Element1)[i])]),TJSObject(Element2[String(TJSObject.keys(ELement2)[i])]))) then
begin
test := false;
end;
end;
end
else if (TJSJSON.stringify(Element1).startsWith('{')) and (TJSJSON.stringify(Element2).startsWith('{')) and (length(TJSObject.keys(Element1)) = length(TJSObject.keys(Element2))) then
begin
for i := 0 to (length(TJSObject.keys(Element1))-1) do
begin
if not(WC_Compare(TJSObject(Element1[String(TJSObject.keys(Element1)[i])]),TJSObject(Element2[String(TJSObject.keys(Element1)[i])]))) then
begin
test := false;
end;
end;
end
else if (TJSJSON.stringify(Element1).startsWith('"')) and (TJSJSON.stringify(Element2).startsWith('"')) and (Element1.toString = Element2.toString) then test := true
else if (Element1.toString = Element2.toString) then test := true
else
begin
test := false;
end;
// if not(test) then console.log(TJSJSON.stringify(Element1)+' <> '+TJSJSON.stringify(Element2));
Result := test;
end;
function PAS_Compare(Element1: TJSONValue; Element2:TJSONValue):Boolean;
var
test: Boolean;
i: integer;
begin
test := true;
if ((Element1.ClassName = 'TJSONNull') and (Element2.Classname = 'TJSONNull')) then test := true
else if ((Element1.ClassName = 'TJSONNull') or (Element2.Classname = 'TJSONNull')) then test := false
else if ((Element1.ClassName = 'TJSONTrue') and (Element2.Classname = 'TJSONTrue')) then test := true
else if ((Element1.ClassName = 'TJSONFalse') and (Element2.Classname = 'TJSONFalse')) then test := true
else if ((Element1.ClassName = 'TJSONString') and (Element2.Classname = 'TJSONString')) and (Element1.ToString = Element2.ToString) then test := true
else if ((Element1.ClassName = 'TJSONNumber') and (Element2.Classname = 'TJSONNumber')) and (Element1.ToString = Element2.ToString) then test := true
else if ((Element1.ClassName = 'TJSONArray') and (Element2.Classname = 'TJSONArray')) and ((Element1 as TJSONArray).Count = (Element2 as TJSONArray).Count) then
begin
for i := 0 to ((Element1 as TJSONArray).Count - 1) do
begin
if not(PAS_Compare((Element1 as TJSONArray).Items[i], ((Element2 as TJSONArray).Items[i]))) then
begin
test := false;
end;
end;
end
else if ((Element1.ClassName = 'TJSONObject') and (Element2.Classname = 'TJSONObject')) and ((Element1 as TJSONOBject).Count = (Element2 as TJSONObject).Count) then
begin
for i := 0 to ((Element1 as TJSONObject).Count - 1) do
begin
if not(PAS_Compare((Element1 as TJSONObject).Pairs[i].JSONValue,(Element2 as TJSONObject).Get((Element1 as TJSONObject).Pairs[i].JSONString.value).JSONValue)) then
begin
test := false;
end;
end;
end
else
begin
if Element1.ToString <> Element2.toString
then test := false
else test := true;
end;
// if not(test) then console.log(Element1.ToString+' <> '+Element2.ToString);
Result := test;
end;
begin
asm var JS_Object1 = JSON.parse(SampleObjectData1); end;
asm var JS_Object2 = JSON.parse(SampleObjectData2); end;
WC_Object1 := TJSJSON.parseObject(SampleObjectData1);
WC_Object2 := TJSJSON.parseObject(SampleObjectData2);
PAS_Object1 := TJSONObject.ParseJSONValue(SampleObjectData1) as TJSONObject;
PAS_Object2 := TJSONObject.ParseJSONValue(SampleObjectData2) as TJSONObject;
asm
function JS_Compare(Element1, Element2) {
var test = true;
if ((Element1 === null) && (Element2 === null)) {test = true}
else if ((Element1 === null) || (Element2 === null)) {test = false}
else if ((Element1 === true) && (Element2 === true)) {test = true}
else if ((Element1 === false) && (Element2 === false)) {test = true}
else if ((Array.isArray(Element1)) && (Array.isArray(Element2)) && (Element1.length == Element2.length)) {
for (var i = 0; i < Element1.length; i++) {
if (!JS_Compare(Element1[i],Element2[i])) {
test = false;
}
}
}
else if ((typeof Element1 === 'string') && (typeof Element2 === 'string') && (Element1 === Element2)) {test = true}
else if ((typeof Element1 === 'number') && (typeof Element2 === 'number') && (Element1 === Element2)) {test = true}
else if ((typeof Element1 === 'object') && (typeof Element2 === 'object') && (Object.keys(Element1).length == Object.keys(Element2).length)) {
for (var key in Element1) {
if (!JS_Compare(Element1[key],Element2[key])) {
test = false;
}
}
}
else {
test = false;
}
// if (!test) {console.log(JSON.stringify(Element1)+' <> '+JSON.stringify(Element2))};
return(test);
}
end;
asm console.log('JS '+JS_Compare(JS_Object1, JS_Object2)); end;
console.log('WC '+BoolToStr(WC_Compare(WC_Object1,WC_Object2),true));
console.log('PAS '+BoolToStr(PAS_Compare(PAS_Object1,PAS_Object2),true));
end;
console.log output:
JS true
WC True
PAS True
24 : Sort JSON Object Elements.
JSON Objects are not ordered, but often it is desirable to display them sorted. Note that we're deliberately not updating the JSON Object with the elements sorted, but rather just iterating through them so that the output is sorted. The gist of this is just extracting all the Keys from the JSON Object, sorting the Keys by whatever method is handy, and then displaying the Keys in their newly sorted order along with the Value corresponding to each.
procedure TForm1.WebButton1Click(Sender: TObject); var WC_Object: TJSObject; PAS_Object: TJSONObject; i: integer; WC_Keys: TStringList; PAS_Keys: TStringList; const SampleObjectData = '{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"}'; begin asm var JS_Object = JSON.parse(SampleObjectData); end; WC_Object := TJSJSON.parseObject(SampleObjectData); PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) as TJSONObject; asm var keys = Object.keys(JS_Object).sort(); for (var i = 0; i < keys.length; i++) { console.log('JS Key: '+keys[i]+' Value: '+JS_Object[keys[i]]) } end; // There are other ways to sort but I didn't have much luck here. // For example, System.Generics.Collections would hang compiler for some reason WC_Keys := TStringList.Create; for i := 0 to length(TJSObject.keys(WC_Object))-1 do WC_Keys.Add(TJSObject.keys(WC_Object)[i]); WC_Keys.sort; for i := 0 to WC_Keys.Count -1 do console.log('WC Key: '+WC_Keys[i]+' Value: '+string(WC_Object[WC_Keys[i]])); // Same approach used here for same reason PAS_Keys := TStringList.Create; for i := 0 to PAS_Object.Count-1 do PAS_Keys.Add(PAS_Object.Pairs[i].JSONString.Value); PAS_Keys.sort; for i := 0 to PAS_Keys.Count -1 do console.log('PAS Key: '+PAS_Keys[i]+' Value: '+PAS_Object.Get(PAS_Keys[i]).JSONValue.ToString); end; console.log output: JS Key: apple Value: fruit JS Key: banana Value: fruit JS Key: carrot Value: vegetable JS Key: orange Value: fruit JS Key: potato Value: vegetable WC Key: apple Value: fruit WC Key: banana Value: fruit WC Key: carrot Value: vegetable WC Key: orange Value: fruit WC Key: potato Value: vegetable PAS Key: apple Value: "fruit" PAS Key: banana Value: "fruit" PAS Key: carrot Value: "vegetable" PAS Key: orange Value: "fruit" PAS Key: potato Value: "vegetable"
25 : Move from JS to WC or PAS.
Let's say that you've used JavaScript to craft a bit of JSON, or happen to have it from some JavaScript code for some other reason, but then want to reference it in Delphi using either the WC or PAS variations that we've been using so far. Here, we define a JS_Object as a Delphi variable of type JSValue so that we can reference it later. And then we access it using just the WC functions and the PAS functions that we've been using up until now, without even having to define variables to hold them.
procedure TForm1.WebButton1Click(Sender: TObject); var JS_Object: JSValue; const SampleObjectData = '{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"}'; begin asm JS_Object = JSON.parse(SampleObjectData); end; // WC: Delphi code to access JavaScript JSON Object // Wrapping it in TJSObject() makes it equivalent to a WC TJSObject console.log('WC: '+TJSJSON.stringify(JS_Object)); console.log('WC: '+IntToStr(length(TJSObject.keys(TJSObject(JS_Object))))); // PAS: Converting to a TJSONObject via Strings works console.log('PAS: '+(TJSONObject.parseJSONValue(TJSJSON.stringify(JS_Object)) as TJSONObject).ToString); console.log('PAS: '+IntToStr((TJSONObject.parseJSONValue(TJSJSON.stringify(JS_Object)) as TJSONObject).Count)); end; console.log output: WC: {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"} WC: 5 PAS: {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"} PAS: 5
26 : Move from WC to JS or PAS.
Here, WC_Object can simply be accessed directly in JS. And the PAS code is basically the same as last time.
procedure TForm1.WebButton1Click(Sender: TObject); var WC_Object: TJSObject; const SampleObjectData = '{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"}'; begin WC_Object := TJSJSON.parseObject(SampleObjectData); // JS: Works as if it were natively defined in JS asm console.log('JS: '+JSON.stringify(WC_Object)); console.log('JS: '+Object.keys(WC_Object).length); end; // PAS: Converting to a TJSONObject via Strings works as before console.log('PAS: '+(TJSONObject.parseJSONValue(TJSJSON.stringify(WC_Object)) as TJSONObject).ToString); console.log('PAS: '+IntToStr((TJSONObject.parseJSONValue(TJSJSON.stringify(WC_Object)) as TJSONObject).Count)); end; console.log output: JS: {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"} JS: 5 PAS: {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"} PAS: 5
27 : Move from PAS to JS or WC.
And here, perhaps you've created some JSON in an VCL app using the TJSONObject variant (PAS) and then find that you want to use it directly in JavaScript or with some other code that uses the WC variant. Interestingly, if you output the PAS TJSONObject while using JS, you'll see the underlying structure and some hints as to why it is more costly to use:
{"fjv":{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"},"fjo$1":{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"},"FMembers":{"FList":{"FList":[],"FCount":0,"FCapacity":0}}}
But it also gives a clue as to how to use it in JS - just reference "fjv" and you'll have your JSON. Converting via strings is also possible here, but there's a convenient JSObject property that is part of TJSONObject, making the conversion to the WC variant almost as easy.
procedure TForm1.WebButton1Click(Sender: TObject); var PAS_Object: TJSONObject; const SampleObjectData = '{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"}'; begin PAS_Object := TJSONObject.parseJSONValue(SampleObjectData) as TJSONObject; // JS asm console.log('JS: '+JSON.stringify(PAS_Object['fjv'])); console.log('JS: '+Object.keys(PAS_Object['fjv']).length); end; // WC console.log('WC: '+TJSJSON.stringify(PAS_Object.JSObject)); console.log('WC: '+IntToStr(length(TJSObject.keys(PAS_Object.JSObject)))); end; console.log output: JS: {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"} JS: 5 WC: {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"} WC: 5
28 : FireDAC to JSON (Simple).
JSON is often used to contain traditional datasets - columns and rows of data that we're perhaps more accustomed to. In this example, we're going to take a sample dataset and show what it looks like when converted to JSON. FireDAC is a popular choice, but any other database likely has a similar mechanism to go from a query or table or some kind of dataset into a JSON representation. Here, the assumption is that this code is running as part of a VCL app as FireDAC itself isn't (entirely?) available within TMS WEB Core directly. In this case, we're using the BatchMove method, where just the data itself is included in the JSON. The sample data is just something I've pulled directly from a project. Nothing particularly interesting or proprietary about it, just a handful of columns and a few records to show what it looks like.
procedure TMyService.GetJSONData(ParametersOfSomeKind: String):TStream; var clientDB: TFDConnection; qry: TFDQuery; bm: TFDBatchMove; bw: TFDBatchMoveJSONWriter; br: TFDBatchMoveDataSetReader; begin Result := TMemoryStream.Create; // Some kind of database connection clientDB := TFDConnection.Create(nil); clientDB.ConnectionDefName := SomeDatabaseConnection; clientDB.Connected := True; // Some kind of query qry.Connection := clientDB; qry.SQL.Text := 'select * from DATAMARK.LUTS;'; qry.Open; // Convert the query to simplified JSON bm := TFDBatchMove.Create(nil); bw := TFDBatchMoveJSONWriter.Create(nil); br := TFDBatchMoveDataSetReader.Create(nil); try br.Dataset := qry; bw.Stream := Result; bm.Reader := br; bm.Writer := bw; bm.Execute; finally br.Free; bw.Free; bm.Free; end; // Cleanup afterwards qry.Free; clientDB.Connected := False; clientDB.Free; end;
The resulting JSON might look something like this.
[{"ID":1,"LOOKUP":0,"SORTORDER":0,"RESPONSE":"Administration","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:42:34","GROUPTYPE":16},{"ID":2,"LOOKUP":1,"SORTORDER":1,"RESPONSE":"Labour","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:47:52","GROUPTYPE":16},{"ID":3,"LOOKUP":2,"SORTORDER":2,"RESPONSE":"IT","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:42:56","GROUPTYPE":16}]
If we run it through a JSON formatter, it becomes a little more legible.
[
{
"ID": 1,
"LOOKUP": 0,
"SORTORDER": 0,
"RESPONSE": "Administration",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:34",
"GROUPTYPE": 16
},
{
"ID": 2,
"LOOKUP": 1,
"SORTORDER": 1,
"RESPONSE": "Labour",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:47:52",
"GROUPTYPE": 16
},
{
"ID": 3,
"LOOKUP": 2,
"SORTORDER": 2,
"RESPONSE": "IT",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:56",
"GROUPTYPE": 16
}
]
This is not terribly efficient in terms of storage - lots of text that is repeated, that sort of thing. So you wouldn't really want to do all your database work exclusively in JSON. However it is compact in the sense that there isn't anything extraneous here - the Keys represent the columns and the Values representing the data. It is an Array so it is easy to figure out how many records there are, and it uses a couple of the supported JSON data types - Strings and Numbers. There's a date/time encoded in my favourite variation of ISO8601.
A dataset formatted this way in JSON is pretty useful as-is. Even if you don't know specifically what the database field types are, you have a pretty good idea. ID, LOOKUP, SORTORDER and GROUPTYPE appear to be Integers. RESPONSE and MODIFIER appear to be Strings. And MODIFIED appears to be a timestamp. We're making assumptions here but sometimes that's plenty sufficient, and sometimes it isn't. But it is simple, so it has that going for it. There are many situations where you could take this data and just run with it without any further considerations.
29 : FireDAC to JSON (Complex).
Similar to the above, we're exporting a dataset into JSON. In this case, FireDAC has its own method for exporting to JSON that also includes metadata that describes the data types of each column and a host of other things that are likely less interesting but useful if moving between FireDAC controls in different environments.
procedure TMyService.GetJSONData(ParametersOfSomeKind: String):TStream; var clientDB: TFDConnection; qry: TFDQuery; begin Result := TMemoryStream.Create; // Some kind of database connection clientDB := TFDConnection.Create(nil); clientDB.ConnectionDefName := SomeDatabaseConnection; clientDB.Connected := True; // Some kind of query qry.Connection := clientDB; qry.SQL.Text := 'select * from sample;'; qry.Open; // Convert the query to FireDAC's JSON qry.SaveToStream(Result, sfJSON); // Cleanup afterwards qry.Free; clientDB.Connected := False; clientDB.Free; end;
Not much to look at though.
{"FDBS":{"Version":15,"Manager":{"UpdatesRegistry":true,"TableList":[{"class":"Table","Name":"DATAMARK.LUTS","SourceName":"DATAMARK.LUTS","SourceID":1,"TabID":0,"EnforceConstraints":false,"MinimumCapacity":50,"ColumnList":[{"class":"Column","Name":"ID","SourceName":"ID","SourceID":1,"DataType":"Int64","Precision":19,"Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"ID","SourcePrecision":19},{"class":"Column","Name":"LOOKUP","SourceName":"LOOKUP","SourceID":2,"DataType":"Int32","Precision":10,"Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OInKey":true,"OriginColName":"LOOKUP","SourcePrecision":10},{"class":"Column","Name":"SORTORDER","SourceName":"SORTORDER","SourceID":3,"DataType":"Int32","Precision":10,"Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"SORTORDER","SourcePrecision":10},{"class":"Column","Name":"RESPONSE","SourceName":"RESPONSE","SourceID":4,"DataType":"AnsiString","Size":200,"Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"RESPONSE","SourcePrecision":200,"SourceSize":200},{"class":"Column","Name":"DESCRIPTION","SourceName":"DESCRIPTION","SourceID":5,"DataType":"AnsiString","Size":250,"Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"DESCRIPTION","SourcePrecision":250,"SourceSize":250},{"class":"Column","Name":"MODIFIER","SourceName":"MODIFIER","SourceID":6,"DataType":"AnsiString","Size":32,"Searchable":true,"FixedLen":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"MODIFIER","SourcePrecision":32,"SourceSize":32},{"class":"Column","Name":"MODIFIED","SourceName":"MODIFIED","SourceID":7,"DataType":"DateTimeStamp","Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"MODIFIED","SourcePrecision":26},{"class":"Column","Name":"GROUPTYPE","SourceName":"GROUPTYPE","SourceID":8,"DataType":"Int32","Precision":10,"Searchable":true,"Base":true,"OInUpdate":true,"OInWhere":true,"OInKey":true,"OriginColName":"GROUPTYPE","SourcePrecision":10}],"ConstraintList":[],"ViewList":[],"RowList":[{"RowID":0,"Original":{"ID":1,"LOOKUP":0,"SORTORDER":0,"RESPONSE":"Administration","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:42:34","GROUPTYPE":16}},{"RowID":1,"Original":{"ID":2,"LOOKUP":1,"SORTORDER":1,"RESPONSE":"Labour","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:47:52","GROUPTYPE":16}},{"RowID":2,"Original":{"ID":3,"LOOKUP":2,"SORTORDER":2,"RESPONSE":"IT","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:42:56","GROUPTYPE":16}}]}],"RelationList":[],"UpdatesJournal":{"Changes":[]}}}}
If we run it through a formatter, it is a little easier to see what is going on.
{
"FDBS": {
"Version": 15,
"Manager": {
"UpdatesRegistry": true,
"TableList": [
{
"class": "Table",
"Name": "DATAMARK.LUTS",
"SourceName": "DATAMARK.LUTS",
"SourceID": 1,
"TabID": 0,
"EnforceConstraints": false,
"MinimumCapacity": 50,
"ColumnList": [
{
"class": "Column",
"Name": "ID",
"SourceName": "ID",
"SourceID": 1,
"DataType": "Int64",
"Precision": 19,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "ID",
"SourcePrecision": 19
},
{
"class": "Column",
"Name": "LOOKUP",
"SourceName": "LOOKUP",
"SourceID": 2,
"DataType": "Int32",
"Precision": 10,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OInKey": true,
"OriginColName": "LOOKUP",
"SourcePrecision": 10
},
{
"class": "Column",
"Name": "SORTORDER",
"SourceName": "SORTORDER",
"SourceID": 3,
"DataType": "Int32",
"Precision": 10,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "SORTORDER",
"SourcePrecision": 10
},
{
"class": "Column",
"Name": "RESPONSE",
"SourceName": "RESPONSE",
"SourceID": 4,
"DataType": "AnsiString",
"Size": 200,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "RESPONSE",
"SourcePrecision": 200,
"SourceSize": 200
},
{
"class": "Column",
"Name": "DESCRIPTION",
"SourceName": "DESCRIPTION",
"SourceID": 5,
"DataType": "AnsiString",
"Size": 250,
"Searchable": true,
"AllowNull": true,
"Base": true,
"OAllowNull": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "DESCRIPTION",
"SourcePrecision": 250,
"SourceSize": 250
},
{
"class": "Column",
"Name": "MODIFIER",
"SourceName": "MODIFIER",
"SourceID": 6,
"DataType": "AnsiString",
"Size": 32,
"Searchable": true,
"FixedLen": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "MODIFIER",
"SourcePrecision": 32,
"SourceSize": 32
},
{
"class": "Column",
"Name": "MODIFIED",
"SourceName": "MODIFIED",
"SourceID": 7,
"DataType": "DateTimeStamp",
"Searchable": true,
"AllowNull": true,
"Base": true,
"OAllowNull": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "MODIFIED",
"SourcePrecision": 26
},
{
"class": "Column",
"Name": "GROUPTYPE",
"SourceName": "GROUPTYPE",
"SourceID": 8,
"DataType": "Int32",
"Precision": 10,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OInKey": true,
"OriginColName": "GROUPTYPE",
"SourcePrecision": 10
}
],
"ConstraintList": [],
"ViewList": [],
"RowList": [
{
"RowID": 0,
"Original": {
"ID": 1,
"LOOKUP": 0,
"SORTORDER": 0,
"RESPONSE": "Administration",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:34",
"GROUPTYPE": 16
}
},
{
"RowID": 1,
"Original": {
"ID": 2,
"LOOKUP": 1,
"SORTORDER": 1,
"RESPONSE": "Labour",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:47:52",
"GROUPTYPE": 16
}
},
{
"RowID": 2,
"Original": {
"ID": 3,
"LOOKUP": 2,
"SORTORDER": 2,
"RESPONSE": "IT",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:56",
"GROUPTYPE": 16
}
}
]
}
],
"RelationList": [],
"UpdatesJournal": {
"Changes": []
}
}
}
}
That's a lot of ground to cover for the same three records but there is absolutely no guesswork when it comes to the data types we're using. Which is what we're after here. Curiously, you can see that there is a DESCRIPTION field in the database, but presumably it didn't have any data so it was excluded from the subsequent records. Just the kind of optimization that happens behind the scenes that will drive a person mad!
30 : JSON to TDataset (Simple).
Using the JSON from the simple variation, everything we've covered applies in terms of looking up data or sorting or whatever else might be needed. Often, one of the goals is to get the JSON data into a local TDataset where we can happily go back to using all of our Delphi tools. In TMS WEB Core, one of the ways to do this is using the TXDataWebDataSet component. You can drop one of these onto a form, and then double-click on it to bring up the familar design-time Fields editor. When you're ready to load data into the dataset, you can call SetJSONData with a JSON Array to load the Dataset. There's also the familiar TWebDataSet and TWebGrid. At design-time, it might look like this.
To get the data into the dataset, there's a SetJSONData() function. There are some caveats here. First, the data has to be in the format that corresponds to the column definitions or it will be excluded. DateTime columns need to be in a specific format or they'll be excluded. Things like that. So a bit tricky to work through the first couple of times. There's also not much in the way of feedback when it gets data it doesn't know what to do with. Here's what it looks like.
procedure TForm1.WebButton1Click(Sender: TObject); var WC_Object: TJSObject; const SampleObjectData = '[{"ID":1,"LOOKUP":0,"SORTORDER":0,"RESPONSE":"Administration","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:42:34","GROUPTYPE":16},'+ '{"ID":2,"LOOKUP":1,"SORTORDER":1,"RESPONSE":"Labour","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:47:52","GROUPTYPE":16},'+ '{"ID":3,"LOOKUP":2,"SORTORDER":2,"RESPONSE":"IT","MODIFIER":"ASIMARD","MODIFIED":"2021-11-17T19:42:56","GROUPTYPE":16}]'; begin WC_Object := TJSJSON.parseObject(SampleObjectData); XDataWebDataSet1.SetJSONData(WC_Object); XdataWebDataSet1.Open; end;
Note that we've done all the hard work in the design-time environment - figuring out what columns we want, how wide they need to be and so on. If things go according to plan, you're ready to go with a grid that might look something like this.
And you're off and running!
31 : JSON to TDataset (Complex).
And at long last we come to the problem that I first encountered when using TMS WEB Core. The basic idea is to use the TMS WEB Core application as a client to, in my case, an XData server. The server sends down JSON data from any of hundreds of different queries. The same thing could be done for any number of other data sources, like weather data and so on, that have nothing to do with XData. The JSON that arrives then needs to be handled on the client.
Initially, the intent was to automatically create a local TDataSet with the data, ready to go. However, without knowing all the field defintions ahead of time, and with not wanting to create them all in design-time, there was no way to load the data using the SetJSONData() call. And at this stage, the JSON data coming in was more of a coummincations mechanism. The fields are defined as a result of the original query, and they'll need to be known when the final UI is presented to the user, but inbetween we really don't care what is going on - JSON is just the name given to the data stream - its contents don't really matter at certain stages.
function GetQueryData(Endpoint: String; Dataset: TXDataWebDataSet):Integer; var Conn: TXDataWebConnection; Client: TXDataWebClient; Response: TXDataClientResponse; JFDBS: TJSObject; JManager: TJSObject; JTableList: TJSObject; JColumnList: TJSArray; JRowList: TJSArray; StringFields: Array of TStringField; IntegerFields: Array of TIntegerField; // Likely need a few more datatypes here i: Integer; begin // Value to indicate the request was unsuccessful Result := -1; // Setup connection to XData Server Conn := TXDataWebConnection.Create(nil); Conn.URL := DM1.CarnivalCoreServer; // could also be a parameter Conn.OnRequest := PopulateJWT; // See below Client := TXDataWebClient.Create(nil); Client.Connection := Conn; await(Conn.OpenAsync); // Make the Request // Likely to have another version of function that includes more endpoint parameters try Response := await(Client.RawInvokeAsync(Endpoint, ['FireDAC'])); except on Error: Exception do begin Client.Free; Conn.Free; console.log('...something is amiss...'); exit; end; end; // Process the FireDAC-specific JSON that was returned JFDBS := TJSObject(TJSObject(TJSJson.Parse(string(Response.Result)))['FDBS']); JManager := TJSObject(JFDBS['Manager']); JTableList := TJSObject(TJSArray(JManager['TableList'])[0]); JColumnList := TJSArray(JTableList['ColumnList']); JRowList := TJSArray(JTableList['RowList']); // Don't really want 'Original' in field names, so let's remove it from JSON first // Probably a better one-liner, but this seems to work for i := 0 to JRowList.Length - 1 do JRowList.Elements[i] := TJSObject(JRowList.Elements[i])['Original']; // We're assuming Dataset parameter is newly created and empty. // First, add all the fields from JSON // NOTE: Very likely more datatypes need to be added here for i := 0 to JColumnList.Length-1 do begin if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'AnsiString') then begin // NOTE: Different datatypes may need different values set (eg: Size for strings) SetLength(StringFields, Length(StringFields) + 1); StringFields[Length(StringFields)-1] := TStringField.Create(Dataset); StringFields[Length(StringFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']); StringFields[Length(StringFields)-1].Size := Integer(TJSObject(JColumnList.Elements[i])['Size']); StringFields[Length(StringFields)-1].Dataset := Dataset; end else if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'Int32') then begin SetLength(IntegerFields, Length(IntegerFields) + 1); IntegerFields[Length(IntegerFields)-1] := TIntegerField.Create(Dataset); IntegerFields[Length(IntegerFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']); IntegerFields[Length(IntegerFields)-1].Dataset := Dataset; end else begin console.log('ERROR: Field ['+String(TJSObject(JColumnList.Elements[i])['Name'])+'] has an unexpected datatype ['+String(TJSObject(JColumnList.Elements[i])['DataType'])+']'); end; end; // Add the data and return the dataset as opened Dataset.SetJSONData(JRowList); Dataset.Open; // Just for fun Result := Dataset.RecordCount; // No dataset stuff to free as the dataset was created by the caller and // all the fields created were created with that dataset as the parent Client.Free; Conn.Free; end;
For me, the mysterious bit was always this:
// Process the FireDAC-specific JSON that was returned
JFDBS := TJSObject(TJSObject(TJSJson.Parse(string(Response.Result)))['FDBS']);
JManager := TJSObject(JFDBS['Manager']);
JTableList := TJSObject(TJSArray(JManager['TableList'])[0]);
JColumnList := TJSArray(JTableList['ColumnList']);
JRowList := TJSArray(JTableList['RowList']);
// Don't really want 'Original' in field names, so let's remove it from JSON first
// Probably a better one-liner, but this seems to work
for i := 0 to JRowList.Length - 1 do
JRowList.Elements[i] := TJSObject(JRowList.Elements[i])['Original'];
JFDBS := TJSObject(TJSObject(TJSJson.Parse(string(Response.Result)))['FDBS']);
This is loading the JSON initially (Response.Result is coming directly from the server) and removing the top-most outer wrapper, 'FDBS'.
JManager := TJSObject(JFDBS['Manager']);
This removes another outer wrapper, 'Manager'.
JTableList := TJSObject(TJSArray(JManager['TableList'])[0]);
This removes another outer wrapper, 'TableList' but still has quite a bit of stuff being carried along.
{
"class": "Table",
"Name": "DATAMARK.LUTS",
"SourceName": "DATAMARK.LUTS",
"SourceID": 1,
"TabID": 0,
"EnforceConstraints": false,
"MinimumCapacity": 50,
"ColumnList": [
{
"class": "Column",
"Name": "ID",
"SourceName": "ID",
"SourceID": 1,
"DataType": "Int64",
"Precision": 19,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "ID",
"SourcePrecision": 19
},
{
"class": "Column",
"Name": "LOOKUP",
"SourceName": "LOOKUP",
"SourceID": 2,
"DataType": "Int32",
"Precision": 10,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OInKey": true,
"OriginColName": "LOOKUP",
"SourcePrecision": 10
},
{
"class": "Column",
"Name": "SORTORDER",
"SourceName": "SORTORDER",
"SourceID": 3,
"DataType": "Int32",
"Precision": 10,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "SORTORDER",
"SourcePrecision": 10
},
{
"class": "Column",
"Name": "RESPONSE",
"SourceName": "RESPONSE",
"SourceID": 4,
"DataType": "AnsiString",
"Size": 200,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "RESPONSE",
"SourcePrecision": 200,
"SourceSize": 200
},
{
"class": "Column",
"Name": "DESCRIPTION",
"SourceName": "DESCRIPTION",
"SourceID": 5,
"DataType": "AnsiString",
"Size": 250,
"Searchable": true,
"AllowNull": true,
"Base": true,
"OAllowNull": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "DESCRIPTION",
"SourcePrecision": 250,
"SourceSize": 250
},
{
"class": "Column",
"Name": "MODIFIER",
"SourceName": "MODIFIER",
"SourceID": 6,
"DataType": "AnsiString",
"Size": 32,
"Searchable": true,
"FixedLen": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "MODIFIER",
"SourcePrecision": 32,
"SourceSize": 32
},
{
"class": "Column",
"Name": "MODIFIED",
"SourceName": "MODIFIED",
"SourceID": 7,
"DataType": "DateTimeStamp",
"Searchable": true,
"AllowNull": true,
"Base": true,
"OAllowNull": true,
"OInUpdate": true,
"OInWhere": true,
"OriginColName": "MODIFIED",
"SourcePrecision": 26
},
{
"class": "Column",
"Name": "GROUPTYPE",
"SourceName": "GROUPTYPE",
"SourceID": 8,
"DataType": "Int32",
"Precision": 10,
"Searchable": true,
"Base": true,
"OInUpdate": true,
"OInWhere": true,
"OInKey": true,
"OriginColName": "GROUPTYPE",
"SourcePrecision": 10
}
],
"ConstraintList": [],
"ViewList": [],
"RowList": [
{
"RowID": 0,
"Original": {
"ID": 1,
"LOOKUP": 0,
"SORTORDER": 0,
"RESPONSE": "Administration",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:34",
"GROUPTYPE": 16
}
},
{
"RowID": 1,
"Original": {
"ID": 2,
"LOOKUP": 1,
"SORTORDER": 1,
"RESPONSE": "Labour",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:47:52",
"GROUPTYPE": 16
}
},
{
"RowID": 2,
"Original": {
"ID": 3,
"LOOKUP": 2,
"SORTORDER": 2,
"RESPONSE": "IT",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:56",
"GROUPTYPE": 16
}
}
]
}
Lots of things in there we don't really need. We'll get to that in a moment. Now we're down to column definitions.
JColumnList := TJSArray(JTableList['ColumnList']);
JRowList := TJSArray(JTableList['RowList']);
Now we're dealing with just the 'RowList' section, which is what we need for SetJSONData to do its thing. This looks like the following. Similar to the simple JSON, which is of course what we're after in terms of the data.
[
{
"RowID": 0,
"Original": {
"ID": 1,
"LOOKUP": 0,
"SORTORDER": 0,
"RESPONSE": "Administration",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:34",
"GROUPTYPE": 16
}
},
{
"RowID": 1,
"Original": {
"ID": 2,
"LOOKUP": 1,
"SORTORDER": 1,
"RESPONSE": "Labour",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:47:52",
"GROUPTYPE": 16
}
},
{
"RowID": 2,
"Original": {
"ID": 3,
"LOOKUP": 2,
"SORTORDER": 2,
"RESPONSE": "IT",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:56",
"GROUPTYPE": 16
}
}
]
// Don't really want 'Original' in field names, so let's remove it from JSON first // Probably a better one-liner, but this seems to work for i := 0 to JRowList.Length - 1 do JRowList.Elements[i] := TJSObject(JRowList.Elements[i])['Original'];
Which results in this, the final form that is imported via SetJSONData:
[
{
"ID": 1,
"LOOKUP": 0,
"SORTORDER": 0,
"RESPONSE": "Administration",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:34",
"GROUPTYPE": 16
},
{
"ID": 2,
"LOOKUP": 1,
"SORTORDER": 1,
"RESPONSE": "Labour",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:47:52",
"GROUPTYPE": 16
},
{
"ID": 3,
"LOOKUP": 2,
"SORTORDER": 2,
"RESPONSE": "IT",
"MODIFIER": "ASIMARD",
"MODIFIED": "2021-11-17T19:42:56",
"GROUPTYPE": 16
}
]
// We're assuming Dataset parameter is newly created and empty.
// First, add all the fields from JSON
// NOTE: Very likely more datatypes need to be added here
for i := 0 to JColumnList.Length-1 do
begin
if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'AnsiString') then
begin
// NOTE: Different datatypes may need different values set (eg: Size for strings)
SetLength(StringFields, Length(StringFields) + 1);
StringFields[Length(StringFields)-1] := TStringField.Create(Dataset);
StringFields[Length(StringFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']);
StringFields[Length(StringFields)-1].Size := Integer(TJSObject(JColumnList.Elements[i])['Size']);
StringFields[Length(StringFields)-1].Dataset := Dataset;
end
else if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'Int32') then
begin
SetLength(IntegerFields, Length(IntegerFields) + 1);
IntegerFields[Length(IntegerFields)-1] := TIntegerField.Create(Dataset);
IntegerFields[Length(IntegerFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']);
IntegerFields[Length(IntegerFields)-1].Dataset := Dataset;
end
else
begin
console.log('ERROR: Field ['+String(TJSObject(JColumnList.Elements[i])['Name'])+'] has an unexpected datatype ['+String(TJSObject(JColumnList.Elements[i])['DataType'])+']');
end;
end;
It is now not so mysterious to see what is going on, with the column data types being pulled from JColumnList and, where necessary, extra information about the data types as well. Since this was written originally, it has been expanded to support a handful more data types that I use regularly, but keeping an eye out for the "unexpected data type" message in console.log is a good idea.
Conclusion.
Well. That about covers it. I think any follow-up questions that are of a technical nature, suggested improvements to the individual blocks of code, and so on should probably be handled via posts in the TMS Support Center where we have a little more control over formatting and threaded responses and so on. If this proves to be a popular topic, then creating a GitHub repository for it may also make some sense.
And I have no doubt at all that there are improvements to be found in many of these examples. More than half of the sections were written just for this article, particularly the WC variants. As I'm relatively new to the WC approach, it is entirely likely I've overlooked, or plainly misunderstood, some of the options available. This was also written with TMS WEB Core 1.9.8.3. No doubt future versions of TMS WEB Core, and the underlying pas2js project, will introduce changes and refinements that will make improvements possible that might not be possible today. TJSONObject is missing a few methods, for example. JavaScript itself has also evolved and will continue to evolve, so improvements there are just as likely, over time.
Andrew Simard.