Out-of-the-box Business Central APIs often use complex types. Addresses on entities and documents, line details, units of measures, journal dimensions, these are just a few examples. There may be more. A typical instance of a complex type looks like this:
The question is: can you do something like this in your own custom APIs?
And the answer is: yes and no.
Let’s start with the “yes” part of the answer.
When you look at how standard API pages do complex types, you can see this:
However, that’s not the entire story. Simply creating a text variable and make it contain serialized JSON content won’t achieve anything. It will show just like any other field bound to a text source, and the API runtime will not automagically translate that to a complex JSON type.
Look behind the address field definition, and you’ll find this:
Obviously, this ODataEDMType makes the field behave the way it does. But what is this POSTALADDRESS ODataEDMType? Make an educated guess. There is a system table 2000000179 OData Edm Type with the following content:
It seems that this table could contain what you are after, but if you try to search for anything using the Web client, you’d find nothing:
However, the good old Object Designer will show you that there is also the OData EDM Definitions page, and if you run this page, the POSTALADDRESS OData EDM type is right there, looking at you:
Click Edit, and you’ll find even more useful info:
And here you have it, there is this XML definition that defines the structure of the POSTALADDRESS complex type, and this is how the API runtime knows how to handle the JSON-serialized text contained in the variable bound to an API-page field configured with this particular OData EDM type.
If you want to achieve something similar with your own APIs, then you need to:
- Create a record in the OData Edm Type table
- Populate the Edm Xml field with the EDM definition that describes your complex type
- Define the ODataEDMType property on the field in the API page
- Write some logic that serializes and deserializes a JSON structure matching the EDM definition you created
I’ve created a little demo to test this out. First, I have a table that keeps information about vehicles:
The table contains the necessary boilerplate needed by the API functionality: the Id field that is automatically assigned during record insertion, and the Last Modified Date Time field that’s updated at every modification.
There are three fields here that describe the engine: Type, Power, and Displacement. I want these fields to be contained in a complex JSON type, simply because it makes more sense for me that way. Just like it makes more sense for Microsoft to group all the address fields inside the POSTALADDRESS complex type.
Here’s my page code:
Apart from the page API boilerplate, the most important piece of logic here is this:
The first function creates a JSON object, populates its properties, and then serializes it into the text variable. The second one deserializes the JSON from the text variable, and then extracts the properties to update the corresponding table fields. These two functions are called when needed.
- For every record retrieval (OnAfterGetRecord)
- After the record has been inserted in the table (OnInsertRecord, because what the API REST invocation returns is the current state of the Rec, or more precise – the actual state of variables and table fields bound to page fields after the insert operation completes)
- After the record has been modified (OnModifyRecord, for the same reason the OnInsertRecord serializes it)
- During insertion, after the record has been inserted into the table, but before it was modified
- During modification, before the rename check is performed
Obviously, for an extension API to work, the OData Edm Type record must be in place when you are first invoking your API, so the obvious way to get the record into the OData Edm Type table is during app installation. I take care of that during extension installation:
My EdmDefinition variable contains the following XML:
<Property Name=”type” Type=”Edm.String” Nullable=”true” />
<Property Name=”power” Type=”Edm.Decimal” Nullable=”true” />
<Property Name=”displacement” Type=”Edm.Decimal” Nullable=”true” />
And, finally, here’s a screenshot of Postman running an API that I created, which uses the concept above to construct a complex JSON type:
I can do insertions and updates with my complex type, too. When I post this:
I get this response:
And to prove it’s not just smoke and mirrors:
This covers the “yes” part of the answer to my question at the beginning of this post. If you remember, I asked if this was possible, and I said “yes, and no”.
Time for the “no” part.
This will work on-prem, but it won’t work in the cloud-based Business Central. The OData Edm Type table is an application table, not a per-tenant table, and to write to that your tenant must be mounted with the permission to write to the application database, and you can imagine that cloud Business Central tenants do not have that permission. Bummer.
And this covers the “no” part. At least this was short.
Now that we’ve covered why this works, and why it doesn’t, it’s time for some musing.
First of all, I firmly believe that the fact that we cannot do complex types from custom APIs in cloud scenarios is simply a design error. There is no reason why we would be limited (in fact: forced) to only use simple types in custom APIs while Microsoft can do complex types as much as they want. Either the OData Edm Type table should be a per-tenant table, or there should be some mechanism (for example, a discovery event, why not?) that would allow us to register our own custom EDM types when we need them.
Second – even though complex types seem convenient and are a nice way of improving semantics and structure of your APIs, they come with a potential problem: they don’t allow partial updates. This means that your payload cannot consist only of the fields you want to update inside a complex type but must include the entire content of the complex type.
Consider this payload of a PATCH call over the customers endpoint:
This will update the phoneNumber and website, and will not trample over or modify the other fields in any way, which is exactly what you would expect:
However, if your payload consists of this:
Then the results leave a bit to be desired:
However, when attempting a partial update of a complex custom type (in my example, engine) behaves differently. The following payload against the vehicles endpoint:
… results in the following error:
I’ve tested quite a few theories regarding this, attempting to resolve it with different EDM declarations, but no matter what I did, it always ends exactly like this last screenshot. My EDM type declaration doesn’t do anything different than, for example, POSTALADDRESS, but my partial update attempts are treated differently than “standard” types (there shouldn’t be anything “standard” about them, because they are not hardcoded, but declared in the OData Edm Type table, just like my type).
And that’s it. This was fun for me to explore, and I learned a thing or two while doing so. I hope you find it useful too.
And, by the way, I’ve created an example of how this works, and I’ve put it at https://github.com/vjekob/customAPI – please feel free to check it there.