Skip to content

nostdahl.com

A technology journal
Article archive
← All articles
Programming

Calling REST APIs and Parsing JSON from Delphi and Lazarus

Bjørn T. Nøstdahl

~5 min read

The service tools I wrote twenty years ago talked to a serial port and a local database. The ones I write now talk to a REST API in Azure, because the device data lives there. Both Delphi and Lazarus handle HTTP and JSON without third-party libraries, and this article shows the same call made with each.

The example fetches the latest readings for one sensor from an endpoint that returns a JSON array:

json
[
  { "sensor": "boiler", "celsius": 58.2, "takenAt": "2024-11-04T07:15:00Z" },
  { "sensor": "boiler", "celsius": 58.6, "takenAt": "2024-11-04T07:20:00Z" }
]

Delphi: THTTPClient and System.JSON

Delphi has two ways to do this. The REST.Client components (TRESTClient, TRESTRequest, TRESTResponse) are great for dropping on a form and wiring to a grid, and the REST Debugger tool that ships with the IDE can generate them for you. For code that lives in a unit with no form, I prefer THTTPClient from System.Net.HttpClient, which is the platform HTTP stack wrapped in one class.

pascal
uses
  System.SysUtils, System.Net.HttpClient, System.Net.URLClient,
  System.NetEncoding, System.JSON;

function FetchReadings(const BaseUrl, Token, Sensor: string): TJSONArray;
var
  Client: THTTPClient;
  Response: IHTTPResponse;
  Value: TJSONValue;
begin
  Client := THTTPClient.Create;
  try
    Client.ConnectionTimeout := 5000;
    Client.ResponseTimeout := 10000;
    Response := Client.Get(
      BaseUrl + '/readings?sensor=' + TNetEncoding.URL.EncodeQuery(Sensor),
      nil,
      [TNetHeader.Create('Authorization', 'Bearer ' + Token),
       TNetHeader.Create('Accept', 'application/json')]);
    if Response.StatusCode <> 200 then
      raise Exception.CreateFmt('API returned %d %s',
        [Response.StatusCode, Response.StatusText]);
    Value := TJSONObject.ParseJSONValue(Response.ContentAsString(TEncoding.UTF8));
    if not (Value is TJSONArray) then
    begin
      Value.Free;
      raise Exception.Create('Expected a JSON array');
    end;
    Result := TJSONArray(Value);
  finally
    Client.Free;
  end;
end;

The caller owns the returned array and frees it when done. Reading values out of it is straightforward thanks to the generic GetValue<T> method on TJSONValue:

pascal
var
  Items: TJSONArray;
  Item: TJSONValue;
begin
  Items := FetchReadings('https://api.example.com', Token, 'boiler');
  try
    for Item in Items do
      WriteLn(Item.GetValue<string>('takenAt'), '  ',
              Item.GetValue<Double>('celsius'):0:1);
  finally
    Items.Free;
  end;
end;

If you would rather map the JSON straight onto a record or class, the REST.Json unit has TJson.JsonToObject<T>, which works well as long as your field names match the JSON keys.

Lazarus: fphttpclient and fpjson

Free Pascal ships fphttpclient for HTTP and fpjson with jsonparser for JSON. One extra unit is needed for HTTPS: adding opensslsockets to the uses clause registers the OpenSSL socket handler, and without it you get a confusing error the first time you hit an https URL.

pascal
uses
  SysUtils, Classes, fphttpclient, opensslsockets, fpjson, jsonparser;

function FetchReadings(const BaseUrl, Token, Sensor: string): TJSONArray;
var
  Client: TFPHTTPClient;
  Body: string;
  Data: TJSONData;
begin
  Client := TFPHTTPClient.Create(nil);
  try
    Client.ConnectTimeout := 5000;
    Client.IOTimeout := 10000;
    Client.AddHeader('Authorization', 'Bearer ' + Token);
    Client.AddHeader('Accept', 'application/json');
    Body := Client.Get(BaseUrl + '/readings?sensor=' + EncodeURLElement(Sensor));
    Data := GetJSON(Body);
    if Data.JSONType <> jtArray then
    begin
      Data.Free;
      raise Exception.Create('Expected a JSON array');
    end;
    Result := TJSONArray(Data);
  finally
    Client.Free;
  end;
end;

Unlike the Delphi client, TFPHTTPClient.Get raises an exception on its own when the server returns an error status, so there is no status check in the code. Wrap the call in try..except where you call it and show the message to the user.

Reading the array uses Objects[] to get each element as a TJSONObject, and Get with a default value to read fields:

pascal
var
  Items: TJSONArray;
  Row: TJSONObject;
  I: Integer;
begin
  Items := FetchReadings('https://api.example.com', Token, 'boiler');
  try
    for I := 0 to Items.Count - 1 do
    begin
      Row := Items.Objects[I];
      WriteLn(Row.Get('takenAt', ''), '  ', Row.Get('celsius', 0.0):0:1);
    end;
  finally
    Items.Free;
  end;
end;

The default-value form is a quiet favourite of mine. A missing field in the response gives you an empty string or a zero instead of an exception, which is usually the right behaviour for telemetry.

Two things that bite in production

Timeouts. Both examples set a connection timeout and a read timeout. Without them, a tool that calls an API from a workshop with a flaky 4G router can hang for minutes, and the engineer will assume it crashed. Five and ten seconds are my defaults; adjust to your network, but never leave them infinite.

Certificate verification. Delphi's client uses the operating system's HTTP stack, so server certificates are checked the way the browser would check them. In the Free Pascal 3.2 series the OpenSSL handler does not verify the server certificate unless you turn verification on and give it a certificate bundle. For a tool that sends a bearer token over the network, an unverified connection is a real weakness, so check how your version behaves and test against a server with a bad certificate before shipping. Does your current tooling fail that test?

Keep the network off the UI thread

Both snippets block while the request runs. In a console program that is fine; in a form it freezes the window, and on a slow link the user will click the button again. The fix is to run the request in a background thread and deliver the result to the UI, which is a big enough topic that it gets its own article next.

Questions for you

  • Do you use the REST components in Delphi, or do you write the HTTP code by hand like I do? What tipped you one way or the other?
  • Have you built a Pascal client for an API that needed OAuth with token refresh? How did you structure it?
  • For Free Pascal users: how do you handle certificate verification today, and did you know it needed handling?

Email me at bjorn.nostdahl@nostdahl.com. I am collecting answers for the threading article, which is where the network calls finally get out of the way of the user.

Continue exploring
← All articlesMore in Programming →