Blog

All Blog Posts  |  Next Post  |  Previous Post

JSON backstage pass for TMS WebCore developers

Bookmarks: 

Tuesday, June 1, 2021

TMS Software Delphi  Components

Guest post about web wizardry with TMS WEB Core

With pleasure, we present today a new guest post. We might go as far as saying that it might change your experience with dealing with JSON in TMS WEB Core apps as Object Pascal developer radically. This wizardry is presented today in this blog by Martin Suer. Martin was already present and enthusiast about TMS WEB Core when we first started showing the product early 2018. Since then, Martin initially started working and doing projects with TMS WEB Core for Delphi and for some time now, he and his colleagues at SymProject GmbH are doing web projects with TMS WEB Core for Visual Studio Code (btw, there is a webinar if you want to learn about it on June 3).
I give the word to Martin. Bookmark this article, it will make your TMS WEB Core application code so much better!


Introduction

Every web frontend developer sooner or later has the pleasure of processing or creating JavaScript objects. In today’s world of web services and microservice architectures, results of function calls are often represented in JSON. To access this as a TMS WEB Core developer, the RTL packages JS and WEBLib.JSON contain many useful classes and functions. With the help of these packages it is possible to convert JSON into JavaScript objects and vice versa, to access single attributes of a JavaScript object or to create JavaScript objects. Dr. Holger Flick has described in his book "TMS WEB Core" in a whole chapter how these RTL packages can be used. (Teaser: The book is worth every cent) Today we will look a little behind the scene and learn about new possibilities beyond that, where we will use the advantages of TMS WEB Core to access JavaScript objects very elegantly and thus combine the advantages of both worlds. As object Pascal developers we are used to work with static type safety. Our data structures contain attributes where the compiler detects errors before runtime. If we try to assign a number to a variable of type string it will be detected by the compiler upfront. In the JavaScript world this is not possible. This gives a JavaScript developer more freedom and flexibility in using variables but also a bigger potential for errors if typos or assumptions about the types of values contained in a variable are incorrect. There errors occur only at runtime. No problem at all, you might think. We use TMS WEB Core and are type safe. But if we access JavaScript objects, we leave the nice guys and visit the dark side…


2. First things first

In the previous section we used the terms JSON and JavaScript object. But what is it exactly and what are the differences? In JavaScript, a variable can contain a simple value as well as an object or an array.

// JavaScript
let i = 5;
let s = 'some text';
let o = {
  a = 'text',
  b = i
}

In this example the variable o contains a JavaScript object with the two attributes a and b where a contains a text and b contains a number.

In TMS WEB Core there is a type JSVALUE for the value of a variable from the JavaScript world. Whenever we want to access the value of a JavaScript variable, we can use the JSVALUE type for it.

// WEBCore

function sampleJavascriptVariable : JSVALUE; assembler;
asm
  let i = 5;
  return i;
end;

procedure AccessJsvalue;
var
  javascriptvalue: JSVALUE;
  i: integer;

begin
  javascriptvalue := sampleJavascriptVariable;
  // value will now contain the JavaScript VALUE (hence JSVALUE) 5
  i := integer(javascriptvalue);
  // we needed to typecast javascriptvalue to integer because the compiler
  // doesn't know the type of the value actually stored in that variable
end;

So whenever we want to access JavaScript values directly from TMS WEB Core, we need a typecast to tell the compiler which type the value in the variable with the type JSVALUE contains. This is already cumbersome with simple types and also error-prone and it gets even more complex when we want to access JavaScript objects.

Let’s summarize: A JavaScript object is an object in the JavaScript world that is stored in an internal representation in a JavaScript variable. To store such a JavaScript object in an Object Pascal variable in TMS WEB Core, the object pascal type JSVALUE is available.

And what has all this to do with JSON ?

JSON (JavaScript Object Notation) is a string that represents a JavaScript object. A JavaScript object can be converted to JSON and a JSON string can be converted to a JavaScript object. More information about the syntax definition of JSON can be found here: https://www.json.org/json-en.html.

Whenever we talk about JSON, we mean a string from an Object Pascal point of view. Whenever we talk about a JavaScript object we mean a JSVALUE from an Object Pascal point of view.

// WEBCore

var
  json: String;
  javascriptobject: JSValue;

begin
  json := '{ "x" : 5, "y" : 7 }';
  javascriptobject := TJSJSON.parse(json);
end;

In this example the JSON variable contains a JSON representation (data type String) of an object with the members x and y, both containing numbers. This string is parsed into a JavaScript object using the static method TJSJSON.parse() and stored in the variable javascriptobject.

Note: All sample sources use only the units that are already automatically included in the uses list of a standard TMS WEB Core web project. So you can try and reproduce everything directly in TMS WEB Core when you include the source texts in the unit1.pas of a newly created project. To keep the example source code short, only necessary code is shown, in real world code you would need to add some exception handling here and there…

All well and good, now we have a JavaScript object in an Object Pascal JSVALUE variable in TMS WEB Core. But how to access it and the individual elements?


3. Practical uses

This is where the backstage pass comes in handy: backstage we learn what the pas2js compiler actually does with the TMS WEB Core source code when our source code is compiled.

type
  TCoordinate = record
    x: double;
    y: double;
  end;

var
  javascriptobject: JSValue;
  json: string;
  coordinate: TCoordinate;

begin
  json := '{ "x" : 5, "y" : 7 }';
  javascriptobject := TJSJSON.parse(json);
  coordinate := TCoordinate(javascriptobject);
  ShowMessage(Format('The coordinate is (%f,%f).',[coordinate.x, coordinate.y]));
end;

Backstage information: The pas2js compiler creates JavaScript code from our Object Pascal source code and finally there is (almost) no difference between a record and a JavaScript object. A record is a JavaScript object in the resulting JavaScript code.

As in the previous example with the typecast from JSVALUE to integer, we can tell the compiler with a typecast from JSVALUE to TCoordinate that we expect an object in the javascriptobject variable that corresponds to the structure of the TCoordinate record type we defined.

In the following ShowMessage() we can then access the values coordinate.x and coordinate.y and it even works with auto code completion.

Let that sink in.

If we tell the compiler what the structure of the JavaScript object looks like by typecasting JSVALUE to an arbitrary record type, we only need one typecast and can then continue programming exactly as if the variable would contain an Object Pascal record. Code completion works and error detection already at compile time. No more cumbersome single accesses with potential typos.

What can we do with this new bag of tricks now?

To clarify this and to understand the following sample code let’s have a quick look at the service https://jsonplaceholder.typicode.com. Here we can retrieve various JSON object representations of sample data through a simple HTTPRequest and use them for testing purposes.

For example, the call https://jsonplaceholder.typicode.com/users/1 returns the following object as JSON:

{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
  "street": "Kulas Light",
  "suite": "Apt. 556",
  "city": "Gwenborough",
  "zipcode": "92998-3874",
  "geo": {
    "lat": "-37.3159",
    "lng": "81.1496"
    }
  },
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
  "name": "Romaguera-Crona",
  "catchPhrase": "Multi-layered client-server neural-net",
  "bs": "harness real-time e-markets"
  }
}

Accessing this data with our new knowledge is now pretty simple:

procedure TMainForm.BtnFetchUserClick(Sender: TObject);
type
  TUser = record
    id: integer;
    name: string;
    email: string;
  end;

var
  request: TJSXMLHttpRequest;
  json: string;
  javascriptobject: JSValue;
  user: TUser;

begin
  WebHTTPRequest.URL := 'https://jsonplaceholder.typicode.com/users/1';
  request := await ( TJSXMLHttpRequest, WebHttpRequest.Perform() );
  json := string(request.response);
  javascriptobject := TJSJSON.parse(json);
  user := TUser(javascriptobject);
  ShowMessage(Format('User %d''s name is %s. You can reach him/her at %s', [user.id, user.name, user.email]));
end;

Note: For this code example to work, we first place a non-visual component TWebHttpRequest on the main form and change its name to WebHttpRequest in the Object Inspector. Additionally, in the declaration of the form it is necessary to mark the BtnFetchUserClick method with the [async] attribute:

type
  TMainForm = class(TWebForm)
    WebHTTPRequest: TWebHTTPRequest;
    [async] 
    procedure BtnFetchUserClick(Sender: TObject);
  end;

In this example we have declared a record type TUser with the fields id, name and email. Using the WebHttpRequest component, we fetch the JSON for a user from the typicode.com web service and store the result in a JSON string variable. Then we convert the obtained JSON to a javascriptobject using TJSJSON.parse(json) and cast it to our TUser type and store it in the user variable.

The variables user and javascriptobject then contain the same values. Once as type TUser and once as JSVALUE. But both reference the same object in memory.

Using the variable user we can now access the individual members of the user and display them in the ShowMessage() method as in this example. When entering the code we are supported by the editor with Code Completion.

The question arises what happened to the rest of the values of the JavaScript object for which we did not define any members in our TUser type declaration?

Well, they are still there, we just can’t access them with our TUser type because the compiler doesn’t know about their existence. With our TUser type declaration we have not changed any data. We only told the compiler how we imagine the representation of the JavaScript object and how we want to access it. A typecast does not result in additional JavaScript code. It is merely information for the compiler that enables it to perform static type checks at compile time.

If we want to access more details of the JavaScript object from the example, we can simply extend our TUser type and get full access to all details:

procedure TMainForm.BtnFetchComplexUserClick(Sender: TObject);
type
  TGeo = record
    lat, lng: string;
  end;

  TAddress = record
    street, suite, city, zipcode: string;
    geo: TGeo;
  end;

  TCompany = record
    name, catchPhrase, bs: string;
  end;

  TUser = record
    id: integer;
    name: string;
    email: string;
    address: TAddress;
    company: TCompany;
    nonExistentField: string;
  end;

var
  request: TJSXMLHttpRequest;
  json: string;
  javascriptobject: JSValue;
  user: TUser;
  doesOrDoesNot, msg: string;

begin
  WebHTTPRequest.URL := 'https://jsonplaceholder.typicode.com/users/3';
  request := await ( TJSXMLHttpRequest, WebHttpRequest.Perform() );
  json := string(request.response);

  javascriptobject := TJSJSON.parse(json);

  user := TUser(javascriptobject);

  if isDefined(user.nonExistentField) then
    doesOrDoesNot := 'does'
  else
    doesOrDoesNot := 'does not';

  ShowMessage( Format('%s lives in %s (Geolocation [%s,%s]) and works for %s. '
    + 'The field "nonExistentField" %s exist.',
    [ user.name, user.address.city, user.address.geo.lat, user.address.geo.lng, user.company.name, doesOrDoesNot ]) );

  ShowMessage('Value of user.nonExistentField is ' + user.nonExistentField);
end;

In the extended example we use a modified TUser type that contains all the details of the JSON supplied by typicode.com and even defines an additional field.

The rest of the example code is the same for now and in the ShowMessage() we can access all the details of the user, including the sub objects. Like such as using user.address.geo.lat to access the coordinate of the address.

Since we have defined a field nonExistendField in our record type, which does not occur in the JSON string at all, the typecast itself won’t be a problem. However, we would get a runtime error if we access such field in the program code assuming that the field exists and contains a valid value. The good news is: with the isDefined function, it is possible to check for the presence of a field value.

Again, no additional code is generated with our typecast. It only tells the compiler which structure of the JavaScript object we expect and how we want to access it. A JavaScript object is a JavaScript object and it is still a JavaScript object after the typecast. BUT we can use it after the typecast as if it were an Object Pascal record.

Of course, this can go wrong if the structure of the JavaScript object does not match the type definition. Is that a problem? It depends…

You usually know what data you expect from a service or in an object. If the assumptions are not correct, you may get a runtime error, but it can be dealt with with exception handling.

Is this an additional new problem I’m introducing with TypeCasts? No, the same problem exists in the pure JavaScript world. (But there it exists even without typecasts) There too, as a developer, you make assumptions about the contents of my data structures. There too, a possible runtime error has to be handled. In the JavaScript world, however, there is no compiler that helps you out with type checks.


4. Going even further

As we have seen, a JavaScript object is easy to use in TMS WEB Core if we have defined a suitable record type for it.

The pas2js compiler (or better transpiler) converts Object Pascal to JavaScript and records become JavaScript objects. But not only that, Object Pascal arrays become JavaScript arrays. Better said: Records are JavaScript objects, Arrays are JavaScript arrays. And since JavaScript arrays are just a special form of JavaScript objects we can take advantage of this and access JavaScript arrays in the same way we access JavaScript objects.

This is illustrated by the following example, where we retrieve a whole list of users via https://jsonplaceholder.typicode.com/users. The corresponding code looks like this:

procedure TMainForm.BtnFetchMultipleUsersClick(Sender: TObject);
type
  TUser = record
    id: integer;
    name: string;
    email: string;
  end;

  TUserArray = array of TUser;

var
  request: TJSXMLHttpRequest;
  json: String;
  javascriptarray: JSValue;
  users: TUserArray;
  user: TUser;

begin
  WebHTTPRequest.URL := 'https://jsonplaceholder.typicode.com/users';
  request := await ( TJSXMLHttpRequest, WebHttpRequest.Perform() );
  json := string(request.response);
  javascriptarray := TJSJSON.parse(json);
  users := TUserArray(javascriptarray);

  user := users[4];

  ShowMessage(Format('Received %d users. User %d''s name is %s. You can reach him/her at %s',
   [Length(users), user.id, user.name, user.email]));
end;

Here we convert the result of the web request using a type cast into a standard array and can thus access all contained user records of the array in a type-safe manner as shown in the ShowMessage() method.


5. The other way round

In the previous examples we always cast JavaScript objects into records to be able to access JavaScript objects easily and comfortably from TMS WEB Core. Often we need to go the other way. If we need to pass more complex data structures to web services, we need to serialize records or objects to pass them to JavaScript functions. Our new principle works fine in this direction as well. Wherever a JSVALUE is expected, we can easily pass a record and this time we don’t even need a typecast, because the receiving side (JavaScript) only knows universal variables, in which arbitrary values or objects can be stored.

Here is a small example:

function AddAToB(value : JSValue): string; assembler;
asm
  value.result = value.a + value.b;
  value.s = 'The sum is ' + value.result;
  return JSON.stringify(value);
end;

procedure TMainForm.BtnTheOtherWayRoundClick(Sender: TObject);
type
  TSampleRecord = record
    a, b, c, result : double;
    s : string;
  end;

var
  sample: TSampleRecord;
  json: string;

begin
  sample.a := 4.5;
  sample.b := 3.2;
  json := AddAToB(sample);

  ShowMessage (json);
end;

In this example, we use the assembler directive to define a JavaScript function AddAToB() that expects a JavaScript object as a parameter. This function does not know the type of the passed object, but it makes assumptions about it in its code (welcome to the dark side). And so it performs an addition of the values value.a and value.b and stores the result in value.result. Also, the result is stored as a string in value.s and the entire value object in JSON representation is returned as the result.

To call this dark side function AddAToB(), we define a TSampleRecord with the values a and b, which we assign numbers to and then use that record as parameter for the function call.

Because of the type declaration, we as callers are not able to preset the values a and b with strings. The type check of the compiler will prevent that.

We can get another interesting information from the function result: The member c is contained in the result and has the value 0. Although the function AddAToB neither knows about the existence of c nor has set the value. This is because we passed our record as a parameter to the function and there the c is defined and during the initialization of the record in the Object Pascal world all members were preset with default values.

So if we use records for values passed to JavaScript functions, we can’t forget to create necessary members in the code, because they are always pre-allocated by the record usage.

We hope that this blog article gave you some interesting insights into the backstage of JSON and JavaScript objects in the TMS WEB Core concert.

Author: Martin Suer

Symproject GmbH https://www.symproject.com




Masiha Zemarai


Bookmarks: 

This blog post has not received any comments yet.



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