In this video, I show some of the tricks that the Variant Data Type offer, and why they’re still useful today, check it out:

In this video, Erik explores the Variant data type in AL — a feature that has been around for at least a decade — and demonstrates how it can be used to emulate generic programming patterns in Business Central. If you’ve ever needed to write a single procedure that can accept any record type, the Variant data type is your friend.
The Problem: Too Many Overloads
Erik starts by describing a common scenario: you need a procedure like LogInformation that can work with any record — Customer, Vendor, Item, or any other table. The naive approach is to create overloaded procedures for each record type:
procedure LogInformation(c: Record Customer)
begin
// do stuff with customer
end;
procedure LogInformation(v: Record Vendor)
begin
// do stuff with vendor
end;
While overloading works, it quickly becomes unmanageable. If you want to use this function for hundreds of tables, you’d need hundreds of overloads — and nobody wants to do that.
The Solution: Variant Data Type
The Variant data type in AL has been around since the early days of Business Central (then NAV). It originally existed to support automation controllers, where parameters weren’t always the same type. Today, it serves as a way to create flexible, generic-style functions.
A Variant variable can hold many different data types. You can inspect what’s inside it using methods like IsRecord, IsBinary, and many others. Microsoft has been quite good about supporting most data types within the Variant.
Using Variant with RecordRef
The key insight is combining Variant with RecordRef. The RecordRef.GetTable method is what Erik calls a “magic function” — while its documentation says it takes a “table” parameter, it can actually accept a Variant. When you pass a Variant containing a record to GetTable, it opens the RecordRef to the correct table and assigns the record that’s hidden inside the Variant.
Here’s the final code that demonstrates the approach:
codeunit 50138 "Variant Test"
{
TableNo = 27;
trigger OnRun()
begin
Message(format(Rec));
end;
procedure Test()
var
c: Record Customer;
v: Record Vendor;
I: Record Item;
D: Date;
begin
D := Today();
C.FindFirst();
V.FindLast();
I.FindFirst();
LogInformation(V);
end;
procedure LogInformation(v: Variant)
var
ref: RecordRef;
c: Record Customer;
codeunitNo: Integer;
begin
if not v.IsRecord then
error('You cannot pass a non-record variable to LogInformation');
ref.GetTable(v);
codeunitNo := 50138;
Codeunit.Run(codeunitNo, v);
end;
}
The LogInformation procedure accepts a Variant, validates that it actually contains a record, and then uses RecordRef.GetTable to work with it generically. You can call it with any record type:
LogInformation(c); // Customer
LogInformation(v); // Vendor
LogInformation(i); // Item
The compiler happily accepts all of these calls because any record variable is implicitly convertible to a Variant.
Protecting Against Bad Input
Since a Variant can hold anything, you need to add guard clauses. If someone passes a Date variable to LogInformation, the compiler won’t complain — but at runtime you’ll get an ugly low-level data conversion error about Microsoft.Dynamics.Nav.Runtime.NavDate not being convertible to INavRecordHandle. The fix is simple:
if not v.IsRecord then
error('You cannot pass a non-record variable to LogInformation');
Converting Back from Variant
The opposite of GetTable is SetTable. You can also convert back from a Variant to a specific record type by simple assignment. For example, if you know the Variant contains a Customer record:
if ref.Number = 18 then // 18 = Customer table
c := v;
After this assignment, c contains the full Customer record that was inside the Variant. You can also go the other direction — assigning a RecordRef back to a Variant with v := ref — to encapsulate and pass it onward.
Variant with Page.Run and Codeunit.Run
The power of Variant extends beyond custom procedures. Several system functions accept Variants where you might expect specific record types.
Page.RunModal with Variant
Page.RunModal(0, v) opens the default lookup page for whatever record type is inside the Variant. If the Variant contains a Vendor record, the Vendor list opens. Change it to an Item, and the Item list opens. This makes it possible to build fully runtime-driven page navigation.
Codeunit.Run with Variant
You can also pass a Variant to Codeunit.Run. This allows both parameters — the codeunit number and the record — to be determined at runtime:
codeunitNo := 50138;
Codeunit.Run(codeunitNo, v);
This eliminates the need for long case statements when dispatching to different codeunits. However, you are still responsible for ensuring the Variant’s record type matches what the target codeunit expects. If there’s a mismatch (say, passing a Vendor record to a codeunit with TableNo = 27, which is Item), you’ll get a runtime error: “Record 23 is not compatible with codeunit run record 27.” Erik notes this is actually a surprisingly clear error message for this kind of scenario.
The Page Extension for Testing
To trigger the test, Erik uses a simple page extension on the Customer List:
pageextension 50138 CustomerListExt extends "Customer List"
{
trigger OnOpenPage();
var
test: Codeunit "Variant Test";
begin
test.Test();
end;
}
Erik is quick to add a disclaimer: don’t open other windows from OnOpenPage in production code — “do as I say, not as I do.” This is purely for demonstration purposes.
Summary
The Variant data type in AL provides a form of generic programming that has been available since at least NAV 2009, with full support by NAV 2013. Here are the key takeaways:
- Variant can hold any data type, including records, dates, integers, and more
- RecordRef.GetTable can accept a Variant, automatically opening the ref to the correct table and assigning the record
- You can convert back from Variant to a specific record type via simple assignment
- System functions like
Page.RunModalandCodeunit.Runaccept Variants where records are expected, enabling fully runtime-driven dispatch - Always validate the Variant’s contents with guards like
v.IsRecordto avoid cryptic runtime errors - While not true generics in the modern programming sense, Variants effectively solve the “too many overloads” problem for record-based operations