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!

 

38 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?

  7. Hello,
    I have been trying to send a post call to braintree or stripe API but have been struggling for weeks now. When I try to get the response from the API I get an authentication 401 error or Misused header name. Make sure request headers are used with HttpRequestMessage, response headers with
    The code I have tried

    content.WriteFrom(‘{ “query”: “query { ping }” }’);
    content.GetHeaders(contentHeaders);
    contentHeaders.Remove(‘Content-Type’);
    contentHeaders.Add(‘Content-Type’, ‘application/json’);
    contentHeaders.Add(‘Authorization’, ‘Bearer sandbox_xxxxxxxxxxx’);
    contentHeaders.Add(‘Braintree-Version’, ‘2019-01-26’);

    if not HttpClient.Post(‘https://payments.sandbox.braintree-api.com/graphql’, content, ResponseMessage)

    When I try this way I get an error

    When I try with
    contentHeaders.TryAddWithoutValidation(‘Authorization’, ‘Bearer sandbox_xxxxxxx’);
    I get an Authentication Error
    P.s I have tested the API using Postman so it’s not the API problem

    Hope you guys can help me
    Thanks in advance

    • I can’t test right now, so it’s just a shot. Could you try to add the authorization header to HttpClient.DefaultRequestHeaders instead of the content headers?

  8. I have tried using but the same problem
    I also have tried using both of them too
    HttpClient.DefaultRequestHeaders.Add(‘Content-Type’, ‘application/json’);
    HttpClient.DefaultRequestHeaders.Add(‘Authorization’, ‘Bearer sandbox_ktxmxhdx_zywjt2d68m9sxjdk’);
    HttpClient.DefaultRequestHeaders.Add(‘Braintree-Version’, ‘2019-01-26’);

    Also I have tried using the same code as the issue below but the same problem
    https://github.com/Microsoft/AL/issues/1911

    Thanks for the fast replay

      • Hello,
        I have been testing today the same issue with the header and find out that the Authorization header is not added to the request
        I have tried adding it both ways using
        HttpClient.DefaultRequestHeaders.Add(‘Authorization’, ‘Bearer sandbox_xxx’);
        HttpHeaders.Add(‘Authorization’, ‘Bearer sandbox_xxx’);

        And using the TryAddWithoutValidation(); but I always got the same issues it is not added to the Request
        It is very strange, I don’t know if these has ever happened to you
        Thanks

        • Okay finally it worked using HttpClient.DefaultRequestHeaders.Add(‘Authorization’, ‘Bearer sandbox_xxx’); not HttpHeaders.Add(‘Authorization’, ‘Bearer sandbox_xxx’);
          Thank for your help

  9. Hello, I am trying to read data from the reponse it comes in this way object inside object
    {
    “data”: {
    “ping”: “pong”
    },
    “extensions”: {
    “requestId”: “xxxxxx”
    }
    }

    I have tried converting it to an object first and it works

    if not JsonObject.ReadFrom(JsonText) then
    Error(‘Invalid response, expected an JSON object’);
    But when i try to print it using as you have done above using
    GetJsonToken(JsonObject, ‘$.data.ping’).AsValue.AsText
    But I get the error Could not find a token with key $. data.ping

    Thanks again

      • Yes correct , also found an alternative solution

        if not JsonObject.ReadFrom(JsonText) then
        Error(‘Invalid response, expected an JSON object’);

        if not JsonObject.Get(‘data’, JsonToken) then
        error(‘Could not find a token with key %1’);

        Message(GetJsonToken(JsonToken.AsObject, ‘ping’).AsValue.AsText);

        Thanks Again

          • Thank you, Might I asked how can i go deeper if there are 3 or 4 nested objects for example like:

            {
            “data”: {
            “tokenizeCreditCard”: {
            “paymentMethod”: {
            “id”: “xxxxxxxx”,
            “details”: {
            “__typename”: “CreditCardDetails”,
            “last4”: “1111”
            }
            }
            }
            }

            Thanks in advance

  10. Hello Kaufmann,
    After succeed making the call in NAV 2015 with your help using stringContent

    I could only read from a single object { “name” :” X” }
    using JsonObject.GetValue(‘name’).ToString

    I have been struggling reading through nested object I have found the solution in AL but it doesn’t work on C/AL NAV 2015
    {
    “data”: {
    “name”: “pong”
    },
    “extensions”: {
    “requestId”: “xxxxxx”
    }
    }

    I have tried using your functions but they don’t work on C/AL
    You help would be appreciated
    Thanks

    • Not sure what your problem is. But in general, reading nested data is a matter of reading the key that contains the nested values into an object.

      For example, let’s say you want to read the requestId inside extensions. You could do that like this:

      JsonObject2 := JsonObject.GetValue(‘extensions’)
      requestId := JsonObject2.GetValue(‘requestId’).ToString

  11. Hello Kaufmann,

    I really appreciate your effort and your post is very useful to me. I just want to know that how can i store all keys from jsonobject.

    {
    “rates”: {
    “CAD”: 1.528,
    “HKD”: 8.3651,
    “ISK”: 155.5,
    “PHP”: 54.667,
    “DKK”: 7.466,
    “HUF”: 365.24,
    “CZK”: 27.603,
    “AUD”: 1.779,
    “RON”: 4.8335,
    “SEK”: 10.9788,
    “IDR”: 17710.73,
    “INR”: 82.106,
    “BRL”: 5.7056,
    “RUB”: 82.481,
    “HRK”: 7.6255,
    “JPY”: 117.54,
    “MYR”: 4.7097
    },
    “base”: “EUR”,
    “date”: “2020-04-06”
    }

    I need all keys and values from this json response. I can successfully access single value by providing single key. But i need all keys and values.

    Thank you,

  12. I got the all keys by using this code

    for i := 1 TO (JsonObject.Keys.Count) do begin
    exchRate.”Currency Code” := Format(JsonObject.Keys.Get(i));
    exchRate.Rate := GetJsonToken(JsonObject, exchRate.”Currency Code”).AsValue().AsDecimal();
    exchRate.”Update Date” := UpdateDate;
    exchRate.Insert();
    end;

    May be this piece of code will be helpful for someone else.

    • Exactly, the Keys property is what you need.
      I would write it a little bit different though.

      forach CurrencyCode in JsonObject.Keys do begin
      exchRate.”Currency Code” := CurrencyCode;
      exchRate.Rate := GetJsonToken(JsonObject, CurrencyCode).AsValue().AsDecimal();

      end;

  13. Pingback: Make your (Dynamics NAV) assistant - Microsoft Dynamics NAV Community

  14. Thanks’ a lot for your samples to get data from a REST web service. This saved me a lot of time. I can’t find templates to modify data with the POST method by AL neither on other sites. Is there anybody out there with a AL code sample to modify data (single and multiple records) using a REST/JSON web service?

    Thanks’ in advance

    • Plenty of examples, but I’m not sure what exactly you are looking for. AL code to modify records in another BC environment?

Leave a Reply to Vikalp Sharma Cancel reply

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