AL support for REST Web Services

24 Jun

A few days ago we did a webinar about the current status of AL in Visual Studio Code. The webinar was organized by NAV Skills and together with Waldo, Erik Hougaard and Daniel Rimmelzwaan we had a full hour of content. And it really felt like we just got started. I had the pleasure to talk about the support of web services in AL language. If you want to watch the recording, then go to the video on Youtube.

The code (including some improvements) that I demonstrated during the webinar is now available on my GitHub repository: https://github.com/ajkauffmann/ALCodeSamples. See the folder “GitHub AL Issues”. Feel free to clone, branch, fork and play with the code.

The code will only run on the June update of the Dynamics NAV Development Preview.

Built-in support for web service calls

With AL code we don’t have support for .Net components. If you want to know more about that, I can recommend to read this blog post from Vjeko. Instead of using .Net components for calling web services, AL has built-in types that map to .Net types. At the moment of writing AL supports:

  • REST web services
  • Handling JSON

Calling REST web services is supported by the HttpClient class. This is basically the same .Net class I have been using in the web services examples that I posted earlier. In this post I introduced the HttpClient. Now we see the same type in AL as a built-in type. This makes a lot of sense and calling web services is a breeze.

For handling JSON we have a set of four types: JsonToken, JsonArray, JsonObject and JsonValue.

The official documentation can be found here: Developer Reference on MSDN. Scroll down to REST API and you will find a list of all supported objects.

With this post I want to introduce a basic example of using HttpClient and JSON objects. The example reads the list of issues from the GitHub repository of the AL language and stores it in a table. Instead optimizing the code I have created just one Codeunit that calls the web service, reads the Json and stores it in the table.

Let me shortly go over some code lines. Here you see the variables I’m using.

ALSupportForRESTWebServices-Variables

As you can see, I’m not using all of the Http classes, only the HttpClient and HttpResponseMessage. On the contrary, all Json classes are used in this example. Well, actually I could avoid some of the Json classes, but I used them to demonstrate their purpose.

HttpClient

Calling the web service is simple and straightforward:

ALSupportForRESTWebServices-CodeExample-1

On line 18 we start with setting a request header called ‘User-Agent’. Without this setting, the call to the web service will fail. This is due to the fact GitHub just rejects calls where this header is missing.

The property DefaultRequestHeaders (documentation on MSDN calls everything a method, but actually a lot of them are properties) returns the class HttpHeaders. This class has a method Add which I use to add that header. It is not needed to first store the HttpHeaders object into a separate variable and then call Add on that variable. As you can see in the code, the Add function is called directly. This kind of code combining is possible since the June update.

The Get method of HttpClient accepts an url and an HttpResponseMessage in which it returns the response message. The Get methods is basically the HTTP GET method and corresponds to opening an URL with your browser. A simple test to see what the web service is returning would be to open the URL in Chrome or Edge. These browser will show (in most cases) the response directly in the browser window.

The Get method returns a boolean that indicates if the call to the web service was succesful. But be careful here, it only indicates if the domain (the base url, in this case https://api.github.com) could be reached and if the call was not rejected by the host. Try this code without the User-Agent header, and the Get will fail.

In case the host could be reached but the path (the remainder after the base url) was incorrect, then it will be indicated in the HttpResponseMessage.

HttpResponseMessage

Let’s have a look at the HttpResponseMessage.

ALSupportForRESTWebServices-CodeExample-2

In line 24 you can see how to test if the web service call was successful. The IsSuccessStatusCode actually checks if the HttpStatusCode is 200, which indicates a successful call. In case the call failed you may find more information in the ReasonPhrase property.

After this, we can get the content of the response. The Content property returns a HttpContent object. The content contains both headers and a body. Because we are only interested in the body we call directly the ReadAs method on the Content property. In this example we use the JsonText variable (a text variable) to indicate the we want to read the content as text. The ReadAs method also accepts an InStream object which enables to receive binary responses as well.

Next step is to parse the JSON response value.

JsonArray

Our first step is to get read the JsonText variable into a JsonArray object using the ReadFrom method.

ALSupportForRESTWebServices-CodeExample-3

 

Why do we use an array here? Well, that depends on the structure of the JSON response. This is something you need to find out before you write the code to handle the JSON. In this case, the JSON structure starts with an [ which indicates it is an array.

ALSupportForRESTWebServices-CodeExample-4

In the array every item is an object. That is indicated by the curly bracket you see on the next level in the JSON text.

On line 37 we start a loop through all objects in the array. At this moment foreach is not supported in AL, so we have to use the good old for … do structure.

The JsonArray.Get method returns a JsonToken. This is a generic container object that represents a well-formed part of JSON data. Basically every JSON object can be represented by a token object. Because we know that every item in the array is an object, we convert the JsonToken to an JsonObject with the function AsObject.

Now we can start reading the properties from the JSON data and store them in the database.

JsonObject and JsonValue

The JSON data contains arrays, objects and values. You can recognize them by in this way:

  • Array is surrounded by square brackets: [ ]
  • Object is surrounded by curly brackets: { }
  • Value has format “key”: “value”

In the JSON data we are using in this example, every GitHub issue is a JSON object. In the JSON object we find values like number, title, state, etc. To get a JSON value out of the JSON object, we use the JsonObject.Get method. This method returns true if the operation was successful. You can see that on line 42 below.

ALSupportForRESTWebServices-CodeExample-5

Note that the Get method returns a JsonToken. This is because a JSON object can contain values, nested objects or arrays. The JsonToken can represent any of them. Next step is to convert the JsonToken to a JsonValue and read the value. In line 45 you can see that we use the AsValue method on the JsonToken and directly call the AsInteger method on the returning JsonValue object.

This is what makes the JsonValue object so powerful: it exposes methods to read the value as integer, as text, as boolean, etc. At the moment of writing the AsDateTime is not working, that’s why line 49 is commented out in the code.

To avoid writing the two lines of code on line 42 and 43 over and over again, I have created a function GetJsonToken that returns the JsonToken or throws an error if not found.

ALSupportForRESTWebServices-CodeExample-6

In line 50 the code uses another feature: selecting a token based on a token path. The function looks like this:

ALSupportForRESTWebServices-CodeExample-7

The SelectToken that is used on line 64 accepts a path. This is a search path that points to a certain token that is nested in the JSON data. Find out more about JSONPath here.

The path in the example is ‘$.user.login’. Take a look at the JSON structure and see that this is a nested object.

ALSupportForRESTWebServices-CodeExample-8

That’s it for now, if you have made it to the end of this post: congratulations, you are a assiduous reader!

Of course I plan to update the previously posted web service examples to AL code as well. Some work is ahead!

 

15 thoughts on “AL support for REST Web Services

  1. Pingback: AL support for REST Web Services - Kauffmann @ Dynamics NAV - Dynamics NAV Users - DUG

  2. Pingback: Make your (Dynamics NAV) assistant – Under the Hood

  3. Pingback: Generating AL objects from JSON - Axians Infoma NAV Blog - Dynamics NAV Users - DUG

  4. Hi Kaufmann,
    Thanks for the post about rest web services – its working good. My JSON output looks like below. How should get the values in my scenario? The code field describe the name of the field and then the value is in value field. Sometimes the value field is not existing if there isnt any data in it like with order_number.

    “header_fields”: [
    {
    “boxes”: [],
    “code”: “order_number”,
    “error”: “”,
    “feature_id”: 21657726
    },
    {
    “boxes”: [],
    “code”: “voucher_number”,
    “error”: “”,
    “feature_id”: 21176595,
    “value”: “38717”
    },
    “boxes”: [],
    “code”: “company_name”,
    “error”: “”,
    “feature_id”: 21164570,
    “value”: “Café & Catering”
    }…continuing

    • Something like this. JsonArray / JsonToken / JsonObject must be declared as variables.


      JsonArray.ReadFrom(responseText);
      foreach JsonToken in JsonArray do begin
        JsonObject := JsonToken.AsObject;
        if JsonObject.Contains('value') then begin
          // do whatever you need, like reading the values and storing it]
        end;
      end;

      • Thanks, it worked.
        I got another question you might be able to help with. The same REST i already have connection to i need to get a access token, but i need to use add keys and values to the body.
        Headers:
        Content-Type application/x-www-form-urlencoded
        Body – see image
        https://imgur.com/4vWD4AH

          • If I understand you correctly, you want to know how to create the content body in AL code?

            Let’s assume ContentText holds the text that you want to send in the body.
            Then the code looks like:


            HttpContent.WriteFrom(ContentText);
            HttpContent.GetHeaders(HttpHeaders);
            HttpHeaders.Remove('Content-Type');
            HttpHeaders.Add('Content-Type', 'application/x-www-form-urlencoded');

            Now you have a HttpContent with the value and the correct header. This can be used in the HttpClient.Post command.

  5. Hi Kaufmann,

    Trying to read and write the data in one D365 BC by using publish D365 BC standard item API. When i send the ‘get’ request method, I got the success response httpstatuscode, which is 200.

    After this when i try to read the json which is given in response from the D365 Business Central /items API. but it is saying “Invalid response, expected an JSON array as root objects’. May is get some help on this. Thanks in Advance.

    This is the JSON i got in postman when i use the same api.

    {
    “@odata.context”: “https://api.businesscentral.dynamics.com/v1.0/MyDomain/sandbox/api/Beta/$metadata#companies(companyid)/items”,
    “value”: [
    {
    “@odata.etag”: “W/\”JzQ0O3NRK1MzYXpnSmlrOUk3ZW5kbHNSdk9hWHZVR21yV2J1YWZjY0VrWk4yd0U9MTswMDsn\””,
    “id”: “43fa647d-6bdc-44cd-8198-156c99a43b89”,
    “number”: “1925-W”,
    “displayName”: “Conference Bundle 1-6”,
    “type”: “Inventory”,
    “itemCategoryId”: “00000000-0000-0000-0000-000000000000”,
    “itemCategoryCode”: “”,
    “blocked”: false,
    “baseUnitOfMeasureId”: “e92db4a7-eb80-44df-b9aa-6d44022bf3b7”,
    “gtin”: “”,
    “inventory”: 0,
    “unitPrice”: 122.5,
    “priceIncludesTax”: true,
    “unitCost”: 0,
    “taxGroupId”: “00000000-0000-0000-0000-000000000000”,
    “taxGroupCode”: “”,
    “lastModifiedDateTime”: “2019-05-26T12:45:02.127Z”,
    “baseUnitOfMeasure”: {
    “code”: “PCS”,
    “displayName”: “Piece”,
    “symbol”: null,
    “unitConversion”: null
    }
    },
    {
    “@odata.etag”: “W/\”JzQ0OzZEQmRsRmxxOHdqNHpFY2d0aUtRM0NOLzh2dXY2dTJMZWhwcE1ndHA5eEU9MTswMDsn\””,
    “id”: “2849f194-e8e3-4af3-b5a8-16e73614eaa6”,
    “number”: “1929-W”,
    “displayName”: “Conference Bundle 1-8”,
    “type”: “Inventory”,
    “itemCategoryId”: “00000000-0000-0000-0000-000000000000”,
    “itemCategoryCode”: “”,
    “blocked”: false,
    “baseUnitOfMeasureId”: “e92db4a7-eb80-44df-b9aa-6d44022bf3b7”,
    “gtin”: “”,
    “inventory”: 0,
    “unitPrice”: 151.7,
    “priceIncludesTax”: true,
    “unitCost”: 0,
    “taxGroupId”: “00000000-0000-0000-0000-000000000000”,
    “taxGroupCode”: “”,
    “lastModifiedDateTime”: “2019-05-26T12:45:02.497Z”,
    “baseUnitOfMeasure”: {
    “code”: “PCS”,
    “displayName”: “Piece”,
    “symbol”: null,
    “unitConversion”: null
    }
    }
    ]
    }

    • Every API call has a unique reply, just like every function call has a unique reply (text, integer, boolean, whatever).
      For that reason, you can’t apply the sample code in this post to the response from a totally different API call.
      Look at the resulting JSON, it starts with an “{“. That means it’s an object, not an array. The sample code calls an API that returns an array, so that’s why you now get an error.
      In this case, you need to get the property value from the JSON. That contains an array (its value starts with “[“)

  6. Hi Kaufmann,

    The code works great. Thank you for the valuable information.
    Just extending on “Chris’s” request above, are we able to write a function in AL to retreive access token before we send requests? I know we can do that in dotNet but since dotNet is out of question, is there a way to write something in Business central?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.