Calling REST APIs and Parsing JSON from Delphi and Lazarus
Bjørn T. Nøstdahl
~8 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:
[
{ "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.
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:
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.
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:
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.
Let the API describe itself: OpenAPI and Swagger
Most APIs I call today publish an OpenAPI description, which many people still call Swagger after the tooling that made it popular. It is a JSON or YAML document that lists every operation, its parameters and the schema of every request and response. ASP.NET Core services in Azure usually serve it at /swagger/v1/swagger.json, and Swagger UI turns it into a page where you can try each call from the browser.
For the readings endpoint above, the relevant part of the document looks like this:
paths:
/readings:
get:
parameters:
- name: sensor
in: query
required: true
schema: { type: string }
responses:
'200':
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Reading' }
components:
schemas:
Reading:
type: object
required: [sensor, celsius, takenAt]
properties:
sensor: { type: string }
celsius: { type: number, format: double }
takenAt: { type: string, format: date-time }
That document is worth more than any amount of prose documentation, and there are three ways to put it to work from Object Pascal.
Use it as the contract. Before writing a line of code, read the description. It told me that takenAt is a date-time string, that celsius is a double, and that all three fields are required, which decides how strict the parsing code should be. Keep a copy of the document next to the client unit and diff it when the API changes; the diff is your change log.
Generate the client in Delphi. Neither OpenAPI Generator nor Swagger Codegen has an Object Pascal target, but the Pascal community has filled the gap. Wagner Landgraf's open source openapi-delphi-generator reads a Swagger 2 or OpenAPI 3 document and writes a typed client: one interface per API, one class per schema, and the HTTP plumbing behind a small runtime that works in both Delphi and Lazarus. mORMot 2 has a generator of its own for both compilers, and there are commercial options if you want someone to call. With a generated client the example collapses to something like this, with names that depend on the generator and the document:
var
Api: IReadingsApi;
Readings: TReadingList;
Reading: TReading;
begin
Api := TReadingsApi.Create('https://api.example.com', Token);
Readings := Api.GetReadings('boiler');
try
for Reading in Readings do
WriteLn(Reading.TakenAt, ' ', Reading.Celsius:0:1);
finally
Readings.Free;
end;
end;
No URL building, no header juggling, no JSON parsing, and a compile error instead of a runtime surprise when the server renames a field. Regenerate the client when the document changes and let the compiler list what broke.
Generate or hand-build in Lazarus. Free Pascal is getting a generator of its own. The fcl-openapi package in the development version includes openapi2pas, a command-line tool that writes client and server units from a description, and a Lazarus package that adds it to the Tools menu. It can be built against the stable 3.2.2 release with some manual copying of its dependencies. Until it ships in a release, a thin client written by hand from the description is a morning's work: one method per operation, one record per schema, and the fphttpclient code from earlier inside.
Publishing your own. If the Pascal program is the server, let it describe itself too. Delphi MVC Framework ships middleware that builds the OpenAPI document from attributes on your controllers and serves Swagger UI next to it. With fpWeb on Free Pascal you serve a hand-written openapi.json and the Swagger UI static files from a route, which is a small price for giving every consumer a browsable, testable description of your API.
One practical habit: Swagger UI's "Try it out" button is the fastest way to confirm that the bearer token and the query parameters are right before you write any Pascal. When the call works there and fails in your code, you know where the bug is.
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?
- Do you generate clients from OpenAPI descriptions or write them by hand? If you generate, which tool, and how often does regenerating break your build?
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.