Skip to main content

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:

  1. MZNFRConsumeRestAPI – Main logic class (API call, deserialization, DB insert)

  2. MZNFRConsumeRestAPIParentContract – Parent contract (wraps JSON list)

  3. MZNFRConsumeRestAPIHeaderContract – Header contract (id, name)

  4. MZNFRConsumeRestAPILinesContract – Child contract (nested data node)

📌 Class Structure Diagram

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;  
   }  
 }  

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


Popular posts from this blog

How to Create a Custom Data Entity in Dynamics 365 Finance and Operations

  Creating a Data Entity in Dynamics 365 Finance and Operations (D365FO) is essential for enabling data import/export, integrations, and public APIs. In this blog, we’ll walk through both the basic shortcut method and the advanced method for creating a custom Data Entity, using a table named MZNFRCustomTable as our example. 🧩 Step 1: Create a Custom Table Start by creating a new table in your project. Go to Solution Explorer → Right-click your project → Add New Item Select Table under FinanceOperations > Data Model Name it MZNFRCustomTable 🧱 Step 2: Add Fields to Your Table Define the fields you need in your table. For example: CustomerAccount CustomerName Currency PaymentNote Balance  Step 3: Shortcut Method – Create Data Entity via Addins Right-click on your table → Addins → Create Data Entity This method is quick but limited in customization. ❗ Common Error If you haven’t created an index, you’ll get this error: "The natural key for the table MZNFRCusto...

Modified Field Method in X++

  In Dynamics 365 Finance and Operations, user experience is everything. If a user enters an ItemId , they shouldn't have to manually type the ItemName . We can automate this using the modifiedField override on the table level. The ModifiedField  method is a kernel method that triggers after a field's value has been changed. It's the perfect place to put logic for fetching related data or resetting dependent fields. The Implementation On your custom table MNZFRSalesLine , you will override the  ModifiedField  method. We use a switch  statement on the _fieldId parameter to ensure our code only runs when the ItemId is the field being touched. public void modifiedField(FieldId _fieldId) { super(_fieldId); switch (_fieldId) { case fieldNum(MNZFRSalesLine, ItemId): if (this.ItemId) { // Fetch the Item Name using the InventTable find method // inventTable.itemName() is a common helper method ...