Blog

All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
Epic JSON Primer (Part 1 of 2)

Bookmarks: 

Thursday, May 5, 2022

Photo of Andrew Simard

As we make our way through various JavaScript libraries, there's a rather important topic that we've touched upon briefly once or twice. But we're about to hit it head-on in a big way. I'm talking of course about JSON. And while JSON isn't specifically a JS library, having a good grasp of JSON is going to be critically important in understanding how to best take advantage of some of the upcoming JS libraries we're going to cover. And it is a popular topic for TMS WEB Core developers in any event!  


This is continued in Epic JSON Primer (Part 2 of 2).


Motivation.

JSON has been around for a long time of course, and has been a part of Delphi for quite a long time as well, long before TMS WEB Core came into being. But using JSON within Delphi, while entirely workable, isn't something that I personally had much experience with, or interest in using, prior to using TMS WEB Core. When writing typical client/server applications with native database drivers connecting to commercial database engines contained within private networks, it honestly doesn't really come up all that much. Connecting a pile of Delphi controls like TDBGrids, TDataSources, TDataSets, TDatabases, TQueries, and so on gets you to where you need to be quickly and efficiently, and there's no JSON to be found. Or even if there was JSON lurking in the depths somewhere, it was at a layer below what we would normally have to deal with.  

Coming into TMS WEB Core, this story changes dramatically. Connections to databases are perhaps more likely to be done through a REST API, even if it involves connecting to the same commercial database engine sitting in the same private network. Native drivers are far less likely to be part of the mix client-side. And while using components like TDataSources and TDataSets is still a viable option, moving generic blocks of data around is far more likely to involve JSON somewhere in the mix than it would have previously. And the closer your development gets to pure JavaScript, the more likely it is that this will end up being the norm rather than the exception. Which turns out to be not a bad thing at all.

Where things might get a little confusing for a TMS WEB Core developer is that JSON can manifest itself in a few ways. And the tools available will depend on the particular manifestation that you're dealing with. We'll be concerning ourselves with three. You may find yourself working with more if you happen to have something like SuperObjects or another JSON component suite as part of your projects.  

  • JS: 100% JavaScript code can be used within asm ... end blocks, using JSON in its native environment.
  • WC: TMS WEB Core-supplied JSON wrappers help keep code in Delphi while more directly utilizing the underlying JavaScript environment, using TJSObject as a starting point.
  • PAS: 100% Delphi that works the same in TMS WEB Core, VCL, and FireMonkey projects, using TJSONObject as a starting point, avoiding JavaScript entirely.

As a TMS WEB Core developer, fortunately, you have the option of working in just one of these environments or moving seamlessly between all three environments depending on your particular needs and preferences. If you're working on a project that moves complex data between a TMS WEB Core client application and a TMS XData server application, it may make sense to use Delphi's native TJSONObject, so the code used to encode and decode JSON can be shared between the two. If you're comfortable working with JavaScript, then perhaps using straight-up 100% pure JavaScript may be a more natural fit. In the many examples that follow, we'll try to keep rigidly to the language of the particular variant. No reason to, other than for demonstration purposes, as mixing freely between them is entirely possible.

Admittedly, there is no shortage of online material that covers JSON. Lots of how-to videos. And more than a few books. Often, the focus is on using JSON in one environment, usually JavaScript. There are comparatively fewer resources that cover using JSON with Delphi, and fewer still that are specific to TMS WEB Core. Not unexpected, of course, given the relative size of each of these developer communities. But there's a common challenge when covering this kind of material, regardless of platform. More in-depth coverage leads to content that is less broadly applicable. Which can be frustrating for those searching for solutions to their increasingly-specific problems. And to be honest, the very same thing is likely to happen here, for the very same reasons. Where to draw the line? 

Like any good story, there is a natural place for a beginning and an ending. Yes, even for JSON! At least for the TMS WEB Core users facing the same challenges I've been facing. The beginning is easy enough, starting with {} and [].  Getting to the end will be a bit of a challenge, no question. Along the way, maybe we'll even manage to pick off a few of the specific problem areas that might be tripping people up.  


01 : So What is JSON?

For our purposes, I like to think of JSON as being just the representation of "sets of things" where the "things" can be of any quantity, type, or size. The syntax for JSON follows just a handful of basic rules.  JSON contains either an Object or an Array to start with. 


TMS Software Delphi  Components

  1. Objects are wrapped in curly brackets {}.
  2. Objects are unordered lists of comma-separated Key:Value pairs.
  3. Keys are wrapped in double quotes, with a colon separating Key and Value.
  4. Arrays are wrapped in square brackets [].
  5. Arrays are ordered lists of Objects, Arrays, or Values.
  6. Values can be strings (wrapped in double quotes).
  7. Values can be numbers (integer or decimal).
  8. Values can be boolean or null (only true, false, and null are permitted here).
  9. Values can be Objects.
  10. Values can be Arrays.

Objects are, by definition, not ordered, though in some instances the insertion order is maintained. Best not to rely upon that. Arrays, by definition, do maintain their order. But it might still be best to have a Key:Value pair that can be used to determine the sort order if necessary. While Object elements are always Key:Value pairs, Array elements can be individual Values, and an Array doesn't need to have the same data types for all of its elements. We'll go through examples of all these things in due course, but that's the gist of the JSON format, and a clue to its popularity - not really any significant restrictions here.  

Note also that the list of supported data types is very short. Values ultimately must be either string, number, boolean, or null. There's nothing here about dates, times, images, currencies, or any other data types that might be important to you. Everything else must be encoded as a string (UTF-8 or one of its wider relatives) and therefore the senders and receivers of JSON data must agree on those conversions. Dates in particular can be troublesome, as even the ISO8601 standard can be legitimately interpreted in different ways, as we shall see closer to the end of this post. Binary data is likely to be encoded as Base64-encoded strings. And data can be endlessly nested to suit nearly any level of complexity.


02 : Defining Objects.

For the rest of this document, we're going to be doing triple the work - showing the JavaScript version (JS), the TMS WEB Core wrapper version (WC), and the Delphi version (PAS) of each scenario, presented as a block of code suitable for inclusion (and tested!) within a TMS WEB Core project, which supports all three of these seamlessly. If you were working on a part of a project that used only Delphi VCL code, you could use just the PAS code equally well there. Or if you were working on a part of a project that used pure JavaScript, then the JS code within the asm ... end blocks would work just as well in that environment. We'll even cover a little bit about moving between these three environments.  

To begin with, we'll need to know how to define variables to reference JSON objects. We'll also need to know how to supply JSON to them (sometimes referred to as serialization), and how to see what they contain (de-serialization). For the sake of simplicity, we're going to show our work using console.log() which works in both Delphi and JavaScript in our TMS WEB Core environment. Simply substitute with ShowMessage or some other equivalent debugging output if you're working in any other environment.  

But why do we have to define variables at all? Well, one of the key differences between JavaScript and Delphi is how data types are managed. Delphi is a strongly-typed language meaning that, at nearly every step, the data type assigned to a variable is known, usually in advance of its being used. And these data types tend to be rather specific in nature. A JSON Object is not necessarily interchangeable with a JSON Array, for example. JavaScript, on the other hand, is a weakly-typed (some might even say non-typed) language. Data types are sort of an afterthought and you can write all kinds of code without having to think about what kind of data is flowing through it.  

There are significant trade-offs to both approaches. JSON is also very closely tied to the JavaScript language itself, and some aspects of the JavaScript language that have evolved over the years have led to significant improvements in using JSON in that environment. The overall result is that the JavaScript code we'll be showing tends to be very short, sometimes a little cryptic, but often very efficient. The Delphi equivalents often have to do a little more work to achieve the same thing, but not always less efficiently, as we shall see.

Using JavaScript code directly in TMS WEB Core just involves wrapping it in an asm ... block. We're not using any other libraries or supporting code to do our work here. The WC variations similarly will work without anything special in terms of the TMS WEB Core environment. The classes we'll be using are there by default, collectively referred to as the TJS* classes. For Delphi though, we do need the extra little step of adding WEBlib.JSON to the uses clause of our project (or Form, etc.).  This brings in the TJSON* classes that we'll be using.

...
uses
  ...
  JS, // Gets us TJS* types (included by default)
  WEBlib.JSON, // Gets us TJSON* types (not included by default)
  ...


Here, then, is our sample WebButtonClick procedure that we'll use in nearly every example that follows. It will start with whatever JS, WC or PAS variables we need and then contain blocks of code pertaining to each. Hopefully in an easy-to-follow manner. Followed by, in most cases, what should be expected as output via console.log(). We'll forgo some of the comments and white-space as we go along to help keep things a little more tidy. But the intent is for each example to stand on its own so you don't have to hunt through previous examples to find missing bits of code.  So yes, a bit repetitive I realize, but with a purpose.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  // Environment: JavaScript (JS)
  // Normally we won't need to define these ahead of time
  // but just in the asm block directly.
  // JSValue is kind of like a variant - we don't know what it is
  JS_Object: JSValue;
  JS_Array:  JSValue;
  // Environment: TMS WEB Core JavaScript Wrapper (WC)
  WC_Object: TJSObject;
  WC_Array: TJSArray;
  // Environment: Delphi (PAS)
  PAS_Object: TJSONObject;
  PAS_Array: TJSONArray;
begin
  // Environment: JavaScript (JS)
  asm
    JS_Object = {};
    JS_Array = [];
    console.log('JS Object = '+JSON.stringify(JS_Object));
    console.log('JS Array = '+JSON.stringify(JS_Array));
  end;
  // Environment: TMS WEB Core JavaScript Wrapper (WC)   WC_Object := TJSObject.new;   WC_Array := TJSArray.new;   console.log('WC Object = '+TJSJSON.stringify(WC_Object));   console.log('WC Array = '+TJSJSON.stringify(WC_Array));
  // Environment: Delphi (PAS)   PAS_Object := TJSONObject.Create;   PAS_Array := TJSONArray.Create;   console.log('PAS Object = '+PAS_Object.ToString);   console.log('PAS Array = '+PAS_Array.ToString); end; console.log output:  JS Object = {} JS Array = [] WC Object = {} WC Array = [] PAS Object = {} PAS Array = []

Nothing too remarkable going on here.  Just for fun, keep the following in mind.  

  • {} is valid JSON
  • [] is valid JSON
  • {{}} is not valid JSON
  • {[]} is not valid JSON
  • [[]] is valid JSON
  • [{}] is valid JSON
  • {"":null} is valid JSON
  • {"":[]} is valid JSON
  • [[],[]] is valid JSON
  • [{},{}] is valid JSON
  • [""] is valid JSON
  • [true,false,null] is valid JSON

If you find yourself working with a lot of random JSON data, using an online tool to validate and/or format JSON to make it easier to read can be pretty handy. There are plenty to choose from, like https://jsonformatter.org/ for example.

Also, an important note about memory allocation. As the main focus here is TMS WEB Core projects, I'm not bothering too much about freeing up any objects that are created. Certainly, there are some areas where this is pretty important, especially when working with desktop apps (VCL and FireMonkey, for example). In JavaScript (which is what TMS WEB Core applications are compiled into, ultimately) this is a bit of a non-issue as there is a garbage collector system that looks after this kind of thing. So I've deliberately left out any kind of object freeing. If you're using any of the PAS code in a desktop app, be sure to free up your objects when you're done with them!


03 : Loading JSON Sample Data.

If you already have JSON data, either in a locally defined string or from some other source, it can be passed directly into a JSON object, even if it hasn't been initialized yet. Here we're using an example that is deliberately longer than the length of a standard Delphi string. WideStrings work well because they can hold a huge amount of information (potentially gigabytes in some cases). Note that in this case, the sample data is an Array. We'll have lots of examples flipping back and forth between Arrays and Objects as the top-level JSON construct.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
  BigSampleData: WideString;
begin
  // Credit: https://gist.github.com/ggordonutech/c04f47fcdad76f8e5e77ee38a9614773
  BigSampleData := '['+
                    '{"id":1,"name":"Banana","description":"A banana is an edible fruit – botanically a berry – produced by several kinds of large herbaceous flowering plants in the genus Musa. In some countries, bananas used for cooking may be called plantains, distinguishing them from dessert banana","photoUrl":"http://www.pngall.com/wp-content/uploads/2016/04/Banana-PNG.png"},'+
                    '{"id":2,"name":"Apple","description":"An apple is a sweet, edible fruit produced by an apple tree. Apple trees are cultivated worldwide and are the most widely grown species in the genus Malus.","photoUrl":"https://images.freshop.com/00852201002228/ad2f58915e3267700906f1025ef8917f_medium.png"},'+
                    '{"id":3,"name":"Peach","description":"The peach is a deciduous tree native to the region of Northwest China between the Tarim Basin and the north slopes of the Kunlun Mountains, where it was first domesticated and cultivated. It bears an edible juicy fruit called a peach or a nectarine.","photoUrl":"http://icons.iconarchive.com/icons/artbees/paradise-fruits/256/Peach-icon.png"},'+
                    '{"id":4,"name":"Strawberry","description":"The fruit is widely appreciated for its characteristic aroma, bright red color, juicy texture, and sweetness.","photoUrl":"https://static.wixstatic.com/media/2cd43b_7415c9b79d664508b6f62a6953403b75~mv2.png/v1/fill/w_256,h_256,fp_0.50_0.50/2cd43b_7415c9b79d664508b6f62a6953403b75~mv2.png"},'+
                    '{"id":5,"name":"Tomato","description":"The tomato is the edible, often red, berry of the plant Solanum lycopersicum, commonly known as a tomato plant. The species originated in western South America and Central America.","photoUrl":"https://www.kampexport.com/sites/kampexport.com/files/images/legume/image/tomates_256_1.jpg"},'+
                    '{"id":6,"name":"Cherry","description":"A cherry is the fruit of many plants of the genus Prunus, and is a fleshy drupe. Commercial cherries are obtained from cultivars of several species, such as the sweet Prunus avium and the sour Prunus cerasus.","photoUrl":"https://cdn.shopify.com/s/files/1/0610/2881/products/cherries.jpg?v=1446676415"}'+
                   ']';
  asm
    var JS_Array = JSON.parse(BigSampleData);
    console.log('JS Array = '+JSON.stringify(JS_Array));
  end;
  WC_Array := TJSArray(TJSJSON.parseObject(BigSampleData));   console.log('WC Array = '+TJSJSON.stringify(WC_Array));
  PAS_Array := TJSONObject.ParseJSONValue(BigSampleData) as TJSONArray;   console.log('PAS Array = '+PAS_Array.ToString); end; console.log output omitted for brevity.


For simpler JSON work, this also shows you how it is possible to create complex JSON objects by way of constructing a string that is properly formatted. Additional elements could be added in a loop for example, without having to use any of the other JSON classes. Just be mindful that you need to have valid JSON when you're done building your string.


04 : Accessing JSON Object Values.

JavaScript doesn't much care about types so its syntax can be considerably simpler. In Delphi, we need to be unambiguous when it comes to types. Here we know it is a string so we just cast it as such, but later we'll have examples where we don't necessarily know what it is at design time, so we'll need to adjust our approach accordingly.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
  SampleObjectData: WideString;
begin
  SampleObjectData := '{"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable"}';
  asm var JS_Object = JSON.parse(SampleObjectData); end;
  WC_Object := TJSJSON.parseObject(SampleObjectData);
  PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) as TJSONObject;
  asm console.log('JS Carrot: '+JS_Object['carrot']); end;   console.log('WC Carrot: '+string(WC_Object['carrot']));   console.log('PAS Carrot: '+(PAS_Object.getValue('carrot') as TJSONString).Value); end; console.log output: JS Carrot: vegetable WC Carrot: vegetable PAS Carrot: vegetable  


05 : Accessing JSON Array Values.

Using JSON Arrays isn't much different than other Delphi arrays, just that the type doesn't need to be set when the array is defined, nor is it fixed for all the elements of the Array. Compared to accessing JSON Objects, the JavaScript code doesn't change much but the Delphi code has to jump through more hoops.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
const
  SampleArrayData = '[{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"}]';
begin
  asm var JS_Array = JSON.parse(SampleArrayData); end;
  WC_Array := TJSArray(TJSJSON.parseObject(SampleArrayData));
  PAS_Array := TJSONObject.ParseJSONValue(SampleArrayData) as TJSONArray;
  asm console.log('JS Array[3] is: '+JS_Array[3].name+' / '+JS_Array[3].type); end;   console.log('WC Array[3] is: '+string(TJSObject(WC_Array[3])['name'])+' / '+string(TJSObject(WC_Array[3])['type']));   console.log('PAS Array[3] is: '+((PAS_Array[3] as TJSONObject).getValue('name') as TJSONString).Value+' / '+((PAS_Array[3] as TJSONObject).getValue('type') as TJSONString).Value); end; console.log output:  JS Array[3] is: carrot / vegetable WC Array[3] is: carrot / vegetable PAS Array[3] is: carrot / vegetable  


06 : Counting JSON Object Elements.

Sometimes you might want to know how many Objects are in your JSON. This is just the first level count and doesn't traverse whatever nested Objects or Arrays you might have. We'll have a look at that a bit later. For the adventurous, JavaScript has a few other tricks up its sleeve when it comes to defining Keys as symbols in JavaScript. But we're going to look past those as they may not be all that generally applicable outside of JavaScript. Check out Object.getOwnPropertySymbols if you think that sort of thing might apply to you.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
 
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 console.log('JS Object Elements: '+Object.keys(JS_Object).length); end;   console.log('WC Object Elements: '+IntToStr(length(TJSObject.keys(WC_Object))));   console.log('PAS Object Elements: '+IntToStr(PAS_Object.count)); end; console.log output: JS Object Elements: 5 WC Object Elements: 5 PAS Object Elements: 5

Note that counting Objects in the first two cases appears to involve extracting all of the key values from the Objects into a separate array, where the length of the array is then reported. I've not looked at the Delphi source to see whether it does the same thing, or whether it just keeps track separately. We'll likely find out in a few more steps when we briefly look at performance.


07 : JSON Array Length.

This is a little easier as Arrays generally are, out of necessity, a little more self-aware.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
const
  SampleArrayData = '[{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"}]';
begin
  asm var JS_Array = JSON.parse(SampleArrayData); end;
  WC_Array := TJSArray(TJSJSON.parseObject(SampleArrayData));
  PAS_Array := TJSONObject.ParseJSONValue(SampleArrayData) as TJSONArray;
  asm console.log('JS Array Length: '+JS_Array.length); end;   console.log('WC Array Length: '+IntToStr(WC_Array.length));   console.log('PAS Array Length: '+IntToStr(PAS_Array.Count)); end; console.log output: JS Array Length: 5 WC Array Length: 5 PAS Array Length: 5


08 : Checking JSON Object Types.

Often JSON will contain data in a format that is, more or less, predictable. For example, we'll be expecting an Array of Objects, or maybe an Object with a bunch of Arrays. We don't usually need to write code to discover what the data types of the contents of the JSON are that we're navigating, especially if we're the ones creating it. This certainly helps simplify things. However, it may come up if you have to process JSON from elsewhere, if you want a little more fault tolerance in your code, or if you're dealing with JSON coming in from the wild and you don't know what to expect.

JavaScript will frequently be quite happy to gloss over any kind of issues with data types. It makes various assumptions and just carries on, sometimes leaving you with gaps in your UI (or worse) where you'd expect data, if everything doesn't fall into place as expected. Delphi on the other hand doesn't take kindly to this kind of thing, so It's likely to stop right in the middle of whatever it is doing if you try to pass off one data type as another in a way that it isn't prepared for. Fortunately, the list of types we're likely to encounter is very short, so checking for them isn't too onerous.  We only have to check for Strings, Numbers, True, False, Null, Array, or Object.  Here then is one way to do this for each of our three environments.  It is a bit tedious of course, but it gets the job done.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
const
  SampleObjectData = '{"a":"some text","b":12,"c":3.14159,"d":["Delphi","JavaScript"],"e":true,"f":false,"g":null,"h":{"somekey":"somevalue"}}';

  function WC_IdentifyType(Something: TJSObject; Key:String):String;
  var i: integer;
  begin
    Result := 'Unknown';
    if (Something[key] = nil) then
    begin
      if string(Something[Key]) = 'null'
      then Result := 'Null'
      else Result := 'KEY NOT FOUND';
    end
    else
    begin
      if      (Something[Key] = True) then Result := 'True'
      else if (Something[Key] = False) then Result := 'False'
      else if (TJSJSON.stringify(Something[Key]).startsWith('"')) then Result := 'String'
      else if (TJSJSON.stringify(Something[Key]).startsWith('{')) then Result := 'Object'
      else if (TJSJSON.stringify(Something[Key]).startsWith('[')) then Result := 'Array'
      else Result := 'Number';
    end;
  end;
  function PAS_IdentifyType(Something: TJSONObject; Key:String):String;   var i: integer;   begin     Result := 'Unknown';     if not(Something.get(Key) is TJSONPair) then     begin       i := 0;       Result := 'KEY NOT FOUND';       while (i <  Something.Count) do       begin         if Something.Pairs[i].ToString.StartsWith('"'+Key+'":') then         begin           Result := 'Null';           break;         end;         i := i +1;       end;     end     else     begin       Result := Copy(Something.get(Key).JSONValue.ClassName, 6, MaxInt);     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_IdentifyType(Something, Key) {       var result = 'Unknown';       if (Something[Key] === undefined)            {result = 'KEY NOT FOUND'}       else if (Something[Key] === null)            {result = 'Null'}       else if (Something[Key] === true)            {result = 'True'}       else if (Something[Key] === false)           {result = 'False'}       else if (Array.isArray(Something[Key]))      {result = 'Array'}       else if (typeof Something[Key] === 'string') {result = 'String'}       else if (typeof Something[Key] === 'number') {result = 'Number'}       else if (typeof Something[Key] === 'object') {result = 'Object'}       return(result);     }     console.log('JS a: '+JS_IdentifyType(JS_Object, 'a'));     console.log('JS b: '+JS_IdentifyType(JS_Object, 'b'));     console.log('JS c: '+JS_IdentifyType(JS_Object, 'c'));     console.log('JS d: '+JS_IdentifyType(JS_Object, 'd'));     console.log('JS e: '+JS_IdentifyType(JS_Object, 'e'));     console.log('JS f: '+JS_IdentifyType(JS_Object, 'f'));     console.log('JS g: '+JS_IdentifyType(JS_Object, 'g'));     console.log('JS h: '+JS_IdentifyType(JS_Object, 'h'));     console.log('JS X: '+JS_IdentifyType(JS_Object, 'X'));   end;
  console.log('WC a: '+WC_IdentifyType(WC_Object, 'a'));   console.log('WC b: '+WC_IdentifyType(WC_Object, 'b'));   console.log('WC c: '+WC_IdentifyType(WC_Object, 'c'));   console.log('WC d: '+WC_IdentifyType(WC_Object, 'd'));   console.log('WC e: '+WC_IdentifyType(WC_Object, 'e'));   console.log('WC f: '+WC_IdentifyType(WC_Object, 'f'));   console.log('WC g: '+WC_IdentifyType(WC_Object, 'g'));   console.log('WC h: '+WC_IdentifyType(WC_Object, 'h'));   console.log('WC X: '+WC_IdentifyType(WC_Object, 'X'));
  console.log('PAS a: '+PAS_IdentifyType(PAS_Object, 'a'));   console.log('PAS b: '+PAS_IdentifyType(PAS_Object, 'b'));   console.log('PAS c: '+PAS_IdentifyType(PAS_Object, 'c'));   console.log('PAS d: '+PAS_IdentifyType(PAS_Object, 'd'));   console.log('PAS e: '+PAS_IdentifyType(PAS_Object, 'e'));   console.log('PAS f: '+PAS_IdentifyType(PAS_Object, 'f'));   console.log('PAS g: '+PAS_IdentifyType(PAS_Object, 'g'));   console.log('PAS h: '+PAS_IdentifyType(PAS_Object, 'h'));   console.log('PAS X: '+PAS_IdentifyType(PAS_Object, 'X')); end; console.log output: JS a: String JS b: Number JS c: Number JS d: Array JS e: True JS f: False JS g: Null JS h: Object JS X: KEY NOT FOUND WC a: String WC b: Number WC c: Number WC d: Array WC e: True WC f: False WC g: Null WC h: Object WC X: KEY NOT FOUND PAS a: String PAS b: Number PAS c: Number PAS d: Array PAS e: True PAS f: False PAS g: Null PAS h: Object PAS X: KEY NOT FOUND


You'll notice how easy this is in JavaScript, largely due to the close relationship between JSON and the JavaScript language generally.


09 : Check if JSON Object Contains a Key.

This can get more complicated when we first have to find the portion of JSON we're interested in, but for now, we'll assume it is just the first level. Also, note that the JSON standard doesn't say anything about duplicate entries in Objects or Arrays. 

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
 
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     if (JS_Object['carrot']) {console.log('JS Object contains carrot')}     else {console.log('JS Object does not contain carrot')}     if (JS_Object['Batman']) { console.log('JS Object contains Batman')}     else {console.log('JS Object does not contain Batman')}   end;
  if WC_Object['carrot'] <> nil   then console.log('WC Object contains carrot')   else console.log('WC Object does not contain carrot');   if WC_Object['Batman'] <> nil   then console.log('WC Object contains Batman')   else console.log('WC Object does not contain Batman');   if PAS_Object.getValue('carrot') <> nil   then console.log('PAS Object contains carrot')   else console.log('PAS Object does not contain carrot');   if PAS_Object.getValue('Batman') <> nil   then console.log('PAS Object contains Batman')   else console.log('PAS Object does not contain Batman'); end; console.log output: JS Object contains carrot JS Object does not contain Batman WC Object contains carrot WC Object does not contain Batman PAS Object contains carrot PAS Object does not contain Batman  


10 : Find String in JSON Array.

Like Objects, we sometimes want to find out if an Array contains a particular element. We also often want to know where it appears in the Array - its index value. By convention, a value of -1 means it isn't in the Array. Making things more complicated is that Arrays may contain very different things. Let's start with just an Array of string values.  Here, JavaScript has a handy indexOf() function that simplifies things quite a bit. Delphi TJSONArray doesn't appear to have that, but it is easy enough to add an equivalent.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
const
  SampleArrayData = '["apple","banana","orange","carrot","potato"]';
  function FindArrString(AJSONArray: TJSONArray; SearchString:String):Integer;   var     i: integer;   begin     Result := -1;     i := 0;     while i < AJSONArray.Count do     begin       if (AJSONArray[i].Value = SearchString) then       begin         Result := i;         break;       end;       i := i + 1;     end;   end;
begin   asm var JS_Array = JSON.parse(SampleArrayData); end;   WC_Array := TJSArray(TJSJSON.parseObject(SampleArrayData));   PAS_Array := TJSONObject.ParseJSONValue(SampleArrayData) as TJSONArray;
  asm    console.log('JS Array: carrot position = '+JS_Array.indexOf('carrot'));    console.log('JS Array: Batman position = '+JS_Array.indexOf('Batman'));   end;
  console.log('WC Array: carrot position = '+IntToStr(WC_Array.indexOf('carrot')));   console.log('WC Array: Batman position = '+IntToStr(WC_Array.indexOf('Batman')));
  console.log('PAS Array: carrot position = '+IntToStr(FindArrString(PAS_Array,'carrot')));   console.log('PAS Array: Batman position = '+IntToStr(FindArrString(PAS_Array,'Batman'))); end; console.log output: JS Array: carrot position = 3 JS Array: Batman position = -1 WC Array: carrot position = 3 WC Array: Batman position = -1 PAS Array: carrot position = 3 PAS Array: Batman position = -1  


11 : Find Key in JSON Array.

It would be convenient if we only had Arrays of string values and nothing more complicated than that, but typically it will almost always be more complicated. The next step up is to find the Array index corresponding to a particular Key. We'll eventually get around to something even more generic and thus more useful a bit later. For now, searching for a Key means that we know that our Array is populated with Objects. JavaScript has a fancy way to do this that doesn't translate particularly well (or at least I have no idea how to write it!) for the WC implementation, so here the approaches for WC and PAS are more or less the same. Note also that an Array of Objects might have more than one Key:Value pair in each Array element, so we'll need to know which one we'll be using as our Key for the Array.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
const
  SampleArrayData = '[{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"}]';

  function WC_FindArrKey(AJSArray: TJSArray; SearchKeyName:String; SearchKey:String):Integer;   var     i: integer;   begin     Result := -1;     i := 0;     while i < AJSArray.Length do     begin       if (TJSObject(AJSArray[i])[SearchKeyName] = SearchKey) then       begin         Result := i;         break;       end;       i := i + 1;     end;   end;
  function PAS_FindArrKey(AJSONArray: TJSONArray; SearchKeyName:String; SearchKey:String):Integer;   var     i: integer;   begin     Result := -1;     i := 0;     while i < AJSONArray.Count do     begin       if (((AJSONArray[i] as TJSONObject).getValue(SearchKeyName) as TJSONSTring).Value = SearchKey) then       begin         Result := i;         break;       end;       i := i + 1;     end;   end;
begin   asm var JS_Array = JSON.parse(SampleArrayData); end;   WC_Array := TJSArray(TJSJSON.parseObject(SampleArrayData));   PAS_Array := TJSONObject.ParseJSONValue(SampleArrayData) as TJSONArray;
  asm    console.log('JS Array: carrot position = '+JS_Array.findIndex(obj => obj.name == 'carrot'));    console.log('JS Array: Batman position = '+JS_Array.findIndex(obj => obj.name == 'Batman'));   end;
  console.log('WC Array: carrot position = '+IntToStr(WC_FindArrKey(WC_Array,'name','carrot')));   console.log('WC Array: Batman position = '+IntToStr(WC_FindArrKey(WC_Array,'name','Batman')));
  console.log('PAS Array: carrot position = '+IntToStr(PAS_FindArrKey(PAS_Array,'name','carrot')));   console.log('PAS Array: Batman position = '+IntToStr(PAS_FindArrKey(PAS_Array,'name','Batman'))); end; console.log output: JS Array: carrot position = 3 JS Array: Batman position = -1 WC Array: carrot position = 3 WC Array: Batman position = -1 PAS Array: carrot position = 3 PAS Array: Batman position = -1


12 : Adding Strings to a JSON Object.

Naturally, JSON Objects are not in any way required to be static. Adding a new element is one of the easier tasks on this list. Here, we're just adding another element similar to the others. Later we'll see how this can be extended to add more complex objects to other more complex objects.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
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 JS_object['pineapple'] = 'fruit'; end;
  WC_Object['pineapple'] := 'fruit';
  PAS_Object.AddPair('pineapple','fruit');

  asm console.log('JS object = '+JSON.stringify(JS_object)); end;   console.log('WC object = '+TJSJSON.stringify(WC_Object));   console.log('PAS object = '+PAS_Object.ToString); end; console.log output: JS object = {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable","pineapple":"fruit"} WC object = {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable","pineapple":"fruit"} PAS object = {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"vegetable","potato":"vegetable","pineapple":"fruit"}


13 : Adding Strings to a JSON Array.

Here we're doing the same thing, but adding elements to an Array instead of an Object. As Arrays are ordered, there's also the option of where in the Array to add an element. The most common choices here would be to add to the end of the Array, to the beginning of the Array, or at a specific point in the Array. In the last case, we'll look up the position of an existing element so we can add a new element just before it.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
  i: integer;
  tmparray: TJSONArray;
const
  SampleArrayData = '[{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"}]';
begin
  asm var JS_Array = JSON.parse(SampleArrayData); end;
  WC_Array := TJSArray(TJSJSON.parseObject(SampleArrayData));
  PAS_Array := TJSONObject.ParseJSONValue(SampleArrayData) as TJSONArray;
  // #1: Add pineapple to the end of the array   // #2: add radish to the beginning of the array   // #3: add peach before carrot in the array
  asm     // #1: Add pineapple to the end of the array     JS_Array.push({"name":"pineapple","type":"fruit"});     // #2: add radish to the beginning of the array     JS_Array.unshift({"name":"radish","type":"vegetable"});     // #3: add peach before carrot in the array     JS_Array.splice(JS_Array.findIndex(obj => obj.name == 'carrot'),0,{"name":"peach","type":"fruit"});   end;
  // #1: Add pineapple to the end of the array   WC_Array.push(TJSJSON.parseObject('{"name":"pineapple","type":"fruit"}'));   // #2: add radish to the beginning of the array   WC_Array.unshift(TJSJSON.parseObject('{"name":"radish","type":"vegetable"}'));   // #3: add peach before carrot in the array   i := 0;   while i < WC_Array.length do   begin     if string(TJSObject(WC_Array[i])['name']) = 'carrot' then     begin       WC_Array.splice(i,0,TJSJSON.parseObject('{"name":"peach","type":"fruit"}'));       break;     end;     i := i + 1;   end;
  // #1: Add pineapple to the end of the array   PAS_Array.Add(TJSONObject.ParseJSONValue('{"name":"pineapple","type":"fruit"}') as TJSONObject);   // #2: add radish to the beginning of the array   tmparray := TJSONObject.ParseJSONValue('[{"name":"radish","type":"vegetable"}]') as TJSONArray;   i := 0;   while i < PAS_Array.count  do   begin     tmparray.Add(Pas_Array[i] as TJSONObject);     i := i + 1;   end;   PAS_Array := tmpArray;   // #3: add peach before carrot in the array   PAS_Array.Add(TJSONObject.ParseJSONValue('{"name":"pineapple","type":"fruit"}') as TJSONObject);   tmparray := TJSONObject.ParseJSONValue('[]') as TJSONArray;   i := 0;   while i < PAS_Array.count  do   begin     if (((Pas_Array[i] as TJSONObject).getValue('name') as TJSONString).Value = 'carrot')     then tmpArray.Add(TJSONObject.ParseJSONValue('{"name":"peach","type":"fruit"}') as TJSONObject);     tmparray.Add(Pas_Array[i] as TJSONObject);     i := i + 1;   end;   PAS_Array := tmpArray;
  asm console.log('JS array = '+JSON.stringify(JS_Array)); end;   console.log('WC array = '+TJSJSON.stringify(WC_Array));   console.log('PAS array = '+PAS_Array.ToString); end; console.log output:  JS array = [{"name":"radish","type":"vegetable"},{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"peach","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"},{"name":"pineapple","type":"fruit"}] WC array = [{"name":"radish","type":"vegetable"},{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"peach","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"},{"name":"pineapple","type":"fruit"}] PAS array = [{"name":"radish","type":"vegetable"},{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"peach","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"},{"name":"pineapple","type":"fruit"}]

A bit more work this time out, as a few of the language disparities between JavaScript and Delphi start to become more apparent. Rather than just syntax, there are basic functions that don't exist for TJSONObject, for example. Not too hard to work around if it is really important, but I think this is a good time to point out that it would probably be best to not assume the Array order is important - just add to it and sort it later. Not always the best approach, but as usual we have options.


14 : Adding Other Types to a JSON Object.

While strings are likely to be the most common data type used when working with JSON, adding the other types is likely to come up from time to time. Note that for desktop applications (VCL And FireMonkey, for example) this is an area that needs a little extra attention. First, these functions should likely be included in a try ... except block as they may attempt to allocate memory and fail to get it, or they may allocate memory and fail to release it afterwards. Not an issue for TMS WEB Core though :-)

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
begin
  asm
    var JS_Object = {};
    JS_Object['a'] = 'some text';
    JS_Object['b'] = 12;
    JS_Object['c'] = 3.14159;
    JS_Object['d'] = ["Delphi","JavaScript"];
    JS_Object['e'] = true;
    JS_Object['f'] = false;
    JS_Object['g'] = null;
    JS_Object['h'] = {"somekey":"somevalue"};
  end;
  WC_Object := TJSObject.new;   WC_Object['a'] := 'some text';   WC_Object['b'] := 12;   WC_Object['c'] := 3.14159;   WC_Object['d'] := TJSArray(TJSJSON.parseObject('["Delphi","JavaScript"]'));   WC_Object['e'] := true;   WC_Object['f'] := false;   WC_Object['g'] := null;   WC_Object['h'] := TJSJSON.parseObject('{"somekey":"somevalue"}');
  PAS_Object := TJSONObject.Create;   PAS_Object.AddPair('a','some text');   PAS_Object.AddPair('b',TJSONNumber.Create(12));   PAS_Object.AddPair('c',TJSONNumber.Create(3.14159));   PAS_Object.AddPair('d',TJSONObject.ParseJSONValue('["Delphi","JavaScript"]') as TJSONArray);   PAS_Object.AddPair('e',TJSONTrue.Create);   PAS_Object.AddPair('f',TJSONFalse.Create);   PAS_Object.AddPair('g',TJSONNull.Create);   PAS_Object.AddPair('h',TJSONObject.ParseJSONValue('{"somekey":"somevalue"}') as TJSONObject);
  asm console.log('JS Object = '+JSON.stringify(JS_Object)); end;   console.log('WC Object = '+TJSJSON.stringify(WC_Object));   console.log('PAS Object = '+PAS_Object.ToString); end; console.log output: JS Object = {"a":"some text","b":12,"c":3.14159,"d":["Delphi","JavaScript"],"e":true,"f":false,"g":null,"h":{"somekey":"somevalue"}} WC Object = {"a":"some text","b":12,"c":3.14159,"d":["Delphi","JavaScript"],"e":true,"f":false,"g":null,"h":{"somekey":"somevalue"}} PAS Object = {"a":"some text","b":12,"c":3.14159,"d":["Delphi","JavaScript"],"e":true,"f":false,"g":null,"h":{"somekey":"somevalue"}}


15 : Delete JSON Object Element.

Here, we're removing a Key/Value pair from an Object. For small JSON Objects, this is likely not a big deal, though it is likely costly relative to some of the other operations if done repeatedly in short loops, for example. In JavaScript this is done, oddly, using a delete statement instead of some kind of function tacked onto the Object itself. For WC and PAS, there doesn't appear to be an equivalent, so we do it the hard way. Note that this also shows an example of one way to iterate through all of the Object elements as well.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
  i: integer;
  tmp_wc_object: TJSObject;
  tmp_pas_object: TJSONObject;
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;
  // Deceptively simple, but why is it a statement and not a function? Oddly inconsistent.   asm delete JS_Object['carrot']; end;
  // No delete option in Delphi   i := 0;   tmp_wc_object := TJSObject.new;   while  i < length(TJSObject.keys(WC_Object)) do   begin     if not(String(TJSObject.keys(WC_Object)[i]) = 'carrot')     then tmp_wc_object[String(TJSObject.keys(WC_Object)[i])] := WC_Object[String(TJSObject.keys(WC_Object)[i])];     i := i + 1;   end;   WC_Object := tmp_wc_object;
  // Wherefore Art Thou TJSONObject.RemovePair?   tmp_pas_object := TJSONObject.Create;   i := 0;   while i < PAS_Object.Count do   begin     if not(PAS_Object.Pairs[i].toString.startsWith('"carrot":'))     then tmp_pas_object.AddPair(PAS_Object.Pairs[i]);     i := i + 1;   end;   PAS_Object := tmp_pas_object;
  asm console.log('JS object = '+JSON.stringify(JS_Object)); end;   console.log('WC object = '+TJSJSON.stringify(WC_Object));   console.log('PAS object = '+PAS_Object.ToString); end; console.log output: JS object = {"apple":"fruit","banana":"fruit","orange":"fruit","potato":"vegetable"} WC object = {"apple":"fruit","banana":"fruit","orange":"fruit","potato":"vegetable"} PAS object = {"apple":"fruit","banana":"fruit","orange":"fruit","potato":"vegetable"}


16 : Delete JSON Array String Element.

Deleting elements from Arrays is perhaps a little unusual given how Arrays are normally used, but what the heck, why not? It is generally not advisable as it is likely that the underlying implementation (well, the JavaScript implementation in this case) the memory consumed by the Array element being deleted may not actually be released but rather just left in the Array as a null value. In WC and PAS, we're actually recreating the Array so this is less of an issue, though the overhead of doing that is arguably much worse. As previously, when deleting JSON Object elements, this is likely harmless on a small scale but if this is happening a lot in a short loop, you might very well want to consider another approach. Here we're just deleting the third element of the Array.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
  i: integer;
  tmp_pas_Array: TJSONArray;
const
  SampleArrayData = '["apple","banana","orange","carrot","potato"]';
begin
  asm var JS_Array = JSON.parse(SampleArrayData); end;
  WC_Array := TJSArray(TJSJSON.parseObject(SampleArrayData));
  PAS_Array := TJSONObject.ParseJSONValue(SampleArrayData) as TJSONArray;
  asm JS_Array.splice(3,1); end;
  WC_Array.splice(3,1);   // No TJSONArray.Remove() ???
  i := 0;   tmp_PAS_Array := TJSONArray.Create;
  while (i < PAS_Array.Count) do   begin     if i <> 3     then tmp_pas_Array.Add(PAS_Array[i].Value);     i := i + 1;   end;
  PAS_Array := tmp_pas_array;
  asm console.log('JS Array = '+JSON.stringify(JS_Array)); end;   console.log('WC Array = '+TJSJSON.stringify(WC_Array));   console.log('PAS Array = '+PAS_Array.ToString); end; console.log output:  JS Array = ["apple","banana","orange","potato"] WC Array = ["apple","banana","orange","potato"] PAS Array = ["apple","banana","orange","potato"]


17 : Delete JSON Array Key.

In this example, let's assume the Array element we want to delete has a particular Value in one of its Keys. This is similar to locating Keys in the Array, with just the extra step of deleting it once we find it. Or in the PAS case, recreating the Array but just not adding the matching element.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Array: TJSArray;
  PAS_Array: TJSONArray;
  i: integer;
  tmparray: TJSONArray;
const
  SampleArrayData = '[{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"carrot","type":"vegetable"},{"name":"potato","type":"vegetable"}]';
begin
  asm var JS_Array = JSON.parse(SampleArrayData); end;
  WC_Array := TJSArray(TJSJSON.parseObject(SampleArrayData));
  PAS_Array := TJSONObject.ParseJSONValue(SampleArrayData) as TJSONArray;
  asm JS_Array.splice(JS_Array.findIndex(obj => obj.name == 'carrot'),1); end;
  i := 0;   while i < WC_Array.length do   begin     if string(TJSObject(WC_Array[i])['name']) = 'carrot' then     begin       WC_Array.splice(i,1);       break;     end;     i := i + 1;   end;
  tmparray := TJSONObject.ParseJSONValue('[]') as TJSONArray;   i := 0;   while i < PAS_Array.count  do   begin     if not(((Pas_Array[i] as TJSONObject).getValue('name') as TJSONString).Value = 'carrot')     then tmparray.Add(Pas_Array[i] as TJSONObject);     i := i + 1;   end;   PAS_Array := tmpArray;
  asm console.log('JS array = '+JSON.stringify(JS_Array)); end;   console.log('WC array = '+TJSJSON.stringify(WC_Array));   console.log('PAS array = '+PAS_Array.ToString); end; console.log output: JS array = [{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"potato","type":"vegetable"}] WC array = [{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"potato","type":"vegetable"}] PAS array = [{"name":"apple","type":"fruit"},{"name":"banana","type":"fruit"},{"name":"orange","type":"fruit"},{"name":"potato","type":"vegetable"}]


18 : Update JSON Object Element.

Deleting an Object element and adding a new one is one approach, particularly if you're changing the name of a Key. But if you're just updating a single Value in a large JSON Object, you can also just update the Value directly.  We'll talk about duplicates in a moment (there are no duplicates?!), but it turns out that in practice there isn't much difference between adding an element and updating an existing element with a new Value.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
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 JS_Object['carrot'] = 'not fruit'; end;

  WC_Object['carrot'] := 'not fruit';

  PAS_Object.AddPair('carrot','not fruit');

  asm console.log('JS object = '+JSON.stringify(JS_Object)); end;
  console.log('WC object = '+TJSJSON.stringify(WC_Object));
  console.log('PAS object = '+PAS_Object.ToString);
end;
console.log output: 
JS object = {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"not fruit","potato":"vegetable"}
WC object = {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"not fruit","potato":"vegetable"}
PAS object = {"apple":"fruit","banana":"fruit","orange":"fruit","carrot":"not fruit","potato":"vegetable"}
  

19 : Duplicate JSON Object Keys.

Not the first surprise I ran across when preparing this material, but a curious one all the same. There's nothing in the JSON standards that make any reference to duplicate Keys. The term 'Key' might imply that it is unique, but that's not even the term used in the standards. And in fact, if you pass JSON with duplicate keys to a formatting or validation tool, it will format and validate it all the same and strip out the duplicate Keys. Importantly, the last Value is used in the case of duplicate Keys. This is surprising to me as "the last Value" really shouldn't have a meaning in set theory but yet here we are. If you treat Key names as actual keys, then this is naturally a non-issue. The fact that it is treated in this way suggests that in fact this should be clarified as one of the rules specified at the outset.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  WC_Object: TJSObject;
  PAS_Object: TJSONObject;
const
  SampleObjectData = '{"some key":"value 1", "some key":"value 2", "some key":"value 3", "some key":"value 4", "some key":"value 5"}';
begin
  asm var JS_Object = JSON.parse(SampleObjectData); end;
  WC_Object := TJSJSON.parseObject(SampleObjectData);
  PAS_Object := TJSONObject.ParseJSONValue(SampleObjectData) as TJSONObject;
  asm console.log('JS Object = '+JSON.stringify(JS_Object)); end;   console.log('WC Object = '+TJSJSON.stringify(WC_Object));   console.log('PAS Object = '+PAS_Object.ToString); end; console.log output: JS object = {"some key":"value 5"} WC object = {"some key":"value 5"} PAS object = {"some key":"value 5"}


Continued in Epic JSON Primer (Part 2 of 2).
Link to GitHub Repository: TMS-WEB-Core-JsonPrimer


Follow Andrew on 𝕏 at @WebCoreAndMore or join our
𝕏
Web Core and More Community.




Andrew Simard


Bookmarks: 

This blog post has received 7 comments.


1. Thursday, May 5, 2022 at 11:36:40 PM

It is a very detailed article. I''m looking forward to the second part. Thank you so much.

Borbor Mehmet Emin


2. Tuesday, May 17, 2022 at 10:34:04 PM

Solution to point 11:
function WC_FindArrKey(pmJSArray: TJSArray; pmSearchKeyName: String; pmSearchKey: String): Integer;
begin
Result := pmJSArray.FindIndex(
function(element: JSValue; index: NativeInt; anArray: TJSArray): Boolean
begin
Result := (TJSObject(element)[pmSearchKeyName] = pmSearchKey);
// or: Result := (TJSObject(anArray[index])[pmSearchKeyName] = pmSearchKey);
end);
end;

tbo


3. Wednesday, May 18, 2022 at 6:10:27 AM

Nicely done!

Simard Andrew


4. Saturday, May 6, 2023 at 11:37:35 PM

There seems a bug in WEBlib.JSON. The function ParseJsonValue is not properly implemented:

class function TJSONObject.ParseJSONValue(const data: string): TJSONValue;
begin
Result := nil;
end;

All of your PAS Examplecode using ParseJSONValue from Weblib.Json get a EAccessviolation after using this function. The result of the function is always nil.

Völker Oliver


5. Sunday, May 7, 2023 at 1:05:39 AM

Do you look at code under “core source” subfolder?

Bruno Fierens


6. Friday, January 26, 2024 at 8:42:53 PM

very nice explainations, now it sounds really clearer.

i''ll opt for the WC option. but then i''m tempted to add this helper to make the code a bit more friendly to read :

WC_Object.KeyCount
instead of
length(TJSObject.keys(WC_Object))

and
WC_Object.GetKey
instead of
(String(TJSObject.keys(WC_Object)[i])
etc..


TJSObjectHelper = class helper for TJSObject
private
procedure SetValue(Index : integer; const aValue : JSValue);
function GetValue(Index : integer) : JSValue;
public
function KeyCount : integer;
property Values[index : integer] : JSValue read GetValue write SetValue;
function GetKey(Index : integer) : string;
function GetPropertyType(Name : String) : TJSValueType;
end;


TJSValueTypeHelper = record helper for TJSValueType
public
function ToString : string;
procedure FromString(const s : string);
end;

here the implementation

{ TJSObjectHelper }

function TJSObjectHelper.GetKey(Index: integer): string;
begin
result := string(TJSObject.keys(self)[index]);
end;

function TJSObjectHelper.GetValue(Index: integer): JSValue;
begin
result := self[GetKey(index)];
end;

function TJSObjectHelper.GetPropertyType(Name : String) : TJSValueType;
begin
result := GetValueType(self[Name]);
end;

function TJSObjectHelper.KeyCount: integer;
begin
result := length(TJSOBject.keys(self));
end;

procedure TJSObjectHelper.SetValue(Index : integer; const aValue : JSValue);
begin
self[GetKey(index)] := aValue;
end;

{ TJSValueTypeHelper }

procedure TJSValueTypeHelper.FromString(const s: string);
begin
if s = ''Boolean'' then
self := jvtBoolean
else if s = ''Integer'' then
self := jvtInteger
else if s = ''Float'' then
self := jvtFloat
else if s = ''String'' then
self := jvtString
else if s = ''Object'' then
self := jvtObject
else if s =''Array'' then
self := jvtArray
else
self := jvtNull;
end;

function TJSValueTypeHelper.ToString: string;
begin
case Self of
jvtNull : result := ''Null'';
jvtBoolean : result := ''Boolean'';
jvtInteger : result := ''Integer'';
jvtFloat : result := ''Float'';
jvtString : result := ''String'';
jvtObject : result := ''Object'';
jvtArray : result := ''Array'';
else result := '''';
end;
end;



Haessig Emmanuel


7. Friday, January 26, 2024 at 8:52:43 PM

Yes, those look pretty great. The trouble with a lot of this JSON stuff is that there are many ways to do the same thing. And of course TMS WEB Core itself evolves over time to make these things easier, surfacing some of this kind of functionality that might not have been there originally. I find myself using the original Delph TJSONObject (PAS variant) approach frequently, as I can use the same code in the XData application where the JSON most often originates for my projects, never really using the WC variant at all.

Of course one of the benefits of having Delphi in the mix means you can do exactly what you''ve done here without any trouble at all. Thanks for posting!

Andrew Simard




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post