Using complex types with custom APIs

  • Post category:Extensions
  • Post comments:24 Comments
  • Reading time:8 mins read

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.

Serialization happens:

  • 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)

Deserialization happens:

  • 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:

<ComplexType Name=”engineType”>
<Property Name=”type” Type=”Edm.String” Nullable=”true” />
<Property Name=”power” Type=”Edm.Decimal” Nullable=”true” />
<Property Name=”displacement” Type=”Edm.Decimal” Nullable=”true” />
</ComplexType>

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.

Vjeko

Vjeko has been writing code for living since 1995, and he has shared his knowledge and experience in presentations, articles, blogs, and elsewhere since 2002. Hopelessly curious, passionate about technology, avid language learner no matter human or computer.

This Post Has 24 Comments

  1. Louis

    Thanks! Very useful information. Great work.

  2. Robert

    Very good article.

    I have 2 questions:
    Is it possible to define EDM Type as an Array? Customer – Invoices
    Is is possible to specify EDM Type inside of EDM Type. Customer – Invoices – Invoice Lines.

    Thank you.

    1. Robert

      I have found answer on my first question.
      In standard page “Native – Sales Inv. Entity”. Lines ODataEMType property here is Collection(NATIVE-SALESINVOICE-LINE). And it seems “Collection” standy behind array.

      1. Mario Longo

        I Robert, did you find answer for your second question? Seems that is not possible to create a API that accept JSON with a nested structure, do you have some news?

  3. Bas

    I’m trying to create a record containing a complex type array (e.g. nativeInvoicingSalesInvoices with the lines property Collection(NAV.ComplexTypes.nativeInvoicingSalesInvoiceLines)). Creating (POST) a new record without any lines works fine.

    If I try to populate the lines property in the same way I can see it in existing records (e.g. lines: [ { quantity = 0 } ] I get the error ”No parameterless constructor defined for this object”. Same error if I try to update (PATCH) the record.

    I can work with the billingPostalAddress complex type without any issue, though.

    Maybe the issue here is that these lines complex types are incomplete (e.g. do not contain the G/L account number) and not meant to be created/updated anyway?

    1. Vjeko

      Hm, I am not sure what would be wrong, I’d need to see the whole code.

  4. Yuriy

    Thanks for the great article!

    I’m wandering is there any workaround for making a dynamic OData complex type? By that I mean a different set of keys in the complex type for the each value. For example I want to be able to get a response body like follows

    {
    “@odata.context”: “…”,
    “value”: [
    {
    “@odata.etag”: “W/\”…””,
    “id”: “…”,
    “pageField”: “…”,

            "complexType": {"keyOne": "...", "keyTwo": "..."} 
    
        },
        {
            "@odata.etag": "W/\"..."",
            "id": "...",
            "pageField": "...",
    
            "complexType": {"differentKeyOne": "...", "differentKeyTwo": "..."} 
    
         }
    ]
    

    }

    1. Vjeko

      Oh, that one… I don’t think it’s possible – but I will gladly stand corrected if you find a way to do it.

      1. Bas

        Yes, I gave up on that one too. We had to switch back to the old API for more detailed integrations. Hope they fix it soon :).

  5. Lo Vc

    Hi Vjeko,

    thanks for this interesting post!

    Have you by any chance had a situation where you created a custom EDM type, with a property that has a type=”Collection(CustomComplexType)”?
    When I try this, requesting the metadata generates an error:
    {
    “error”: {
    “code”: “Unknown”,
    “message”: “The OData EDM complex type Collection(POSTALADDRESS) that is referenced by Name was not found. CorrelationId: 8d25d468-5212-452f-bd0f-f9337fc6cf23.”
    }
    }

    Can this be because BC is trying to parse the type value to a Microsoft.NAV.’CustomType’ string?

    1. Vjeko

      Hi Lorenzo,

      Sorry, I haven’t had such a situation. It could be because of the reason you mention, but it could be because of something else. Right now, I have really no time to investigate.

    2. Gintautas

      Hi Lorenzo,
      Have you found a way to have collection within the complex type? Dealing with the same issue.

  6. davidroys

    Hi Vjeko – I know this is an old topic, but it looks like the complex types have been removed from the standard APIs in BC 17. Do you know if there’s something in the works to replace this? I thought that a good way for the product team to solve the complex types in API problem would be to simply allow API page parts to be added to an API page. Each API page part also have API Page parts. As long as they don’t make them a different page type, you could reference the sub-entity directly through a URL or get to it via the parent. This way (you would hope) the partial updates would all just work as well. I think I might suggest this as a BC Idea. Take care Vjeko.

    1. Vjeko

      Hi Dave,

      Unfortunately, I wasn’t aware that this change was made, but I think it was intentional. They were half-baked, and apparently they decided to drop that, rather than go all-in. Your suggestion sounds – well – sound 🙂 By all means, suggest it, and if I see that suggestion, I’ll vote it up!

      All the best old pal!

    2. Sergio Castelluccio

      Hi, from where did you see that complex types have been removed? I’ve BC17 installed in docker and I can see them in for example Customer Entity page.

    1. Vjeko

      Yeah, as I replied on another comment. It was really an undocumented hack.

  7. Silas

    Hello,

    Microsoft has deprecated the ODataEDMType Property and set the Table “OData Edm Type” and Pages to Read-only.

    My issue now is that i have an API for Warehouse Shipment Header, with the Subpage “Warehouse Shipment Lines”. I now need a third level for Lot Information. In the Past I would have created an ODataEDMType for the Lot Information, which is not possible anymore.

    How does Microsoft handle that now?

    Best regards,
    Silas

    1. Vjeko

      Sorry, I really don’t know the answer. The ODataEDMType was mostly an undocumented hack (for all of us except for Microsoft), and I’d say that you now simply need to do OData deep inserts and updates.

Leave a Reply