Consuming an External REST API in Dynamics 365 Finance & Operations (X++) and Saving Data into a Custom Table
Introduction
In this blog, I will explain how I consumed an online REST API in Microsoft Dynamics 365 Finance & Operations (D365 FO) using X++ and CLR Interop, deserialized the JSON response using Data Contract classes, and finally stored the data in a custom table.
This is a common real-world integration scenario where external systems expose data via REST APIs and D365 FO needs to consume and persist that data.
For this demo, I used the following public REST API:
API URL:
https://api.restful-api.dev/objects
Solution Overview
The solution consists of four X++ classes:
-
MZNFRConsumeRestAPI – Main logic class (API call, deserialization, DB insert)
-
MZNFRConsumeRestAPIParentContract – Parent contract (wraps JSON list)
-
MZNFRConsumeRestAPIHeaderContract – Header contract (id, name)
-
MZNFRConsumeRestAPILinesContract – Child contract (nested
datanode)
API Response Structure
The API returns a JSON array like this:
[
{
"id": "1",
"name": "Google Pixel 6 Pro",
"data": {
"color": "Cloudy White",
"capacity": "128 GB"
}
}
]
Because D365 FO cannot deserialize a raw JSON array directly, we wrap it inside a root node before deserialization.
1️⃣ Main Logic Class
MZNFRConsumeRestAPI
This class is responsible for:
-
Calling the REST API
-
Reading the response
-
Wrapping JSON
-
Deserializing into contracts
-
Saving data into a custom table
✅ Full Code
internal final class MZNFRConsumeRestAPI
{
public static void main(Args _args)
{
System.Net.HttpWebRequest webRequest;
System.Net.HttpWebResponse webresponse;
System.IO.Stream responseStream;
System.IO.StreamReader reader;
System.Exception ex;
str output;
ListEnumerator listEnumerator;
MZNFRConsumeRestAPITable consumeRestAPITable;
try
{
new InteropPermission(InteropKind::ClrInterop).assert();
// Call REST API
webRequest = System.Net.WebRequest::Create("https://api.restful-api.dev/objects");
webRequest.Method = "GET";
webRequest.ContentType = "application/json";
webresponse = webRequest.GetResponse();
responseStream = webresponse.GetResponseStream();
reader = new System.IO.StreamReader(responseStream);
output = reader.ReadToEnd();
// Wrap JSON array inside root node
str finalJson = '{\"_contract\":' + output + '}';
// Deserialize JSON
MZNFRConsumeRestAPIParentContract parentContract = FormJsonSerializer::deserializeObject(classNum(MZNFRConsumeRestAPIParentContract), finalJson);
List listData = parentContract.parmContract();
if (listData)
{
listEnumerator = listData.getEnumerator();
while (listEnumerator.moveNext())
{
MZNFRConsumeRestAPIHeaderContract headerContract = listEnumerator.current();
consumeRestAPITable.clear();
consumeRestAPITable.ID = headerContract.parmId();
consumeRestAPITable.Name = headerContract.parmName();
MZNFRConsumeRestAPILinesContract linesContract = headerContract.parmLinesContract();
if (linesContract)
{
consumeRestAPITable.Color = linesContract.parmColor();
consumeRestAPITable.Capacity = linesContract.parmCapacity();
consumeRestAPITable.Price = linesContract.parmPrice();
consumeRestAPITable.Generation = linesContract.parmGeneration();
consumeRestAPITable.CPUModel = linesContract.parmCPUModel();
consumeRestAPITable.StrapColour = linesContract.parmStrapColour();
consumeRestAPITable.Description = linesContract.parmDescription();
consumeRestAPITable.HardDiskSize = linesContract.parmHardDiskSize();
consumeRestAPITable.CaseSize = linesContract.parmCaseSize();
consumeRestAPITable.ScreenSize = linesContract.parmScreenSize();
consumeRestAPITable.CapacityGB = linesContract.parmCapacityGB();
consumeRestAPITable.Year = linesContract.parmYear();
}
consumeRestAPITable.insert();
}
info("Data imported successfully from API.");
}
else
{
warning("No data found in API.");
}
}
catch
{
ex = CLRInterop::getLastException().GetBaseException();
error(ex.get_Message());
}
}
}
2️⃣ Parent Contract Class
MZNFRConsumeRestAPIParentContract
This contract wraps the full JSON list.
✅ Full Code
[DataContractAttribute]
class MZNFRConsumeRestAPIParentContract
{
private List contractLoc;
[DataMember("_contract"),
DataCollection(Types::Class,classStr(MZNFRConsumeRestAPIHeaderContract)),
AifCollectionType("_contractLoc",Types::Class,classStr(MZNFRConsumeRestAPIHeaderContract))]
public List parmContract(List _contractLoc = contractLoc)
{
if (!prmIsDefault(_contractLoc))
{
contractLoc = _contractLoc;
}
return contractLoc;
}
}
[DataContractAttribute]
class MZNFRConsumeRestAPIParentContract
{
private List contractLoc;
[DataMember("_contract"),
DataCollection(Types::Class,classStr(MZNFRConsumeRestAPIHeaderContract)),
AifCollectionType("_contractLoc",Types::Class,classStr(MZNFRConsumeRestAPIHeaderContract))]
public List parmContract(List _contractLoc = contractLoc)
{
if (!prmIsDefault(_contractLoc))
{
contractLoc = _contractLoc;
}
return contractLoc;
}
}
3️⃣ Header Contract Class
MZNFRConsumeRestAPIHeaderContract
This maps the top-level JSON fields.
✅ Full Code
[DataContractAttribute]
class MZNFRConsumeRestAPIHeaderContract
{
private str id;
private str name;
private MZNFRConsumeRestAPILinesContract linesContract;
[DataMember("id")]
public str parmId(str _id = id)
{
if (!prmIsDefault(_id))
{
id = _id;
}
return id;
}
[DataMember("name")]
public str parmName(str _name = name)
{
if (!prmIsDefault(_name))
{
name = _name;
}
return name;
}
[DataMember("data")]
public MZNFRConsumeRestAPILinesContract
parmLinesContract(
MZNFRConsumeRestAPILinesContract _linesContract = linesContract)
{
if (!prmIsDefault(_linesContract))
{
linesContract = _linesContract;
}
return linesContract;
}
} 4️⃣ Lines (Child) Contract Class
MZNFRConsumeRestAPILinesContract
This contract maps the nested data object.
✅ Full Code
[DataContractAttribute]
class MZNFRConsumeRestAPILinesContract
{
real price;
str CPUModel;
str HardDiskSize;
str year;
str color;
str capacity;
str generation;
str StrapColour;
str CaseSize;
str Description;
str ScreenSize;
str capacityGB;
[DataMember("price")]
public real parmPrice(real _price = price)
{
if (!prmIsDefault(_price))
{
price = _price;
}
return price;
}
[DataMember("CPU model")]
public str parmCPUModel(str _CPUModel = CPUModel)
{
if (!prmIsDefault(_CPUModel))
{
CPUModel = _CPUModel;
}
return CPUModel;
}
[DataMember("Hard disk size")]
public str parmHardDiskSize(str _HardDiskSize = HardDiskSize)
{
if (!prmIsDefault(_HardDiskSize))
{
HardDiskSize = _HardDiskSize;
}
return HardDiskSize;
}
[DataMember("year")]
public str parmYear(str _year = year)
{
if (!prmIsDefault(_year))
{
year = _year;
}
return year;
}
[DataMember("color")]
public str parmColor(str _color = color)
{
if (!prmIsDefault(_color))
{
color = _color;
}
return color;
}
[DataMember("Capacity")]
public str parmCapacity(str _capacity = capacity)
{
if (!prmIsDefault(_capacity))
{
capacity = _capacity;
}
return capacity;
}
[DataMember("Generation")]
public str parmGeneration(str _generation = generation)
{
if (!prmIsDefault(_generation))
{
generation = _generation;
}
return generation;
}
[DataMember("Strap Colour")]
public str parmStrapColour(str _StrapColour = StrapColour)
{
if (!prmIsDefault(_StrapColour))
{
StrapColour = _StrapColour;
}
return StrapColour;
}
[DataMember("Case Size")]
public str parmCaseSize(str _CaseSize = CaseSize)
{
if (!prmIsDefault(_CaseSize))
{
CaseSize = _CaseSize;
}
return CaseSize;
}
[DataMember("Description")]
public str parmDescription(str _Description = Description)
{
if (!prmIsDefault(_Description))
{
Description = _Description;
}
return Description;
}
[DataMember("Screen size")]
public str parmScreenSize(str _ScreenSize = ScreenSize)
{
if (!prmIsDefault(_ScreenSize))
{
ScreenSize = _ScreenSize;
}
return ScreenSize;
}
[DataMember("capacity GB")]
public str parmCapacityGB(str _capacityGB = capacityGB)
{
if (!prmIsDefault(_capacityGB))
{
capacityGB = _capacityGB;
}
return capacityGB;
}
}
Conclusion
This implementation shows a clean and scalable approach to:
-
Consume REST APIs in D365 FO
-
Handle JSON arrays correctly
-
Use Parent–Child Data Contracts
-
Deserialize complex JSON
-
Save external data into custom tables
This pattern can easily be extended for:
-
Authenticated APIs
-
POST / PUT requests
-
Batch jobs
-
Scheduled integrations
