DEV Community

Alexey Panteleev
Alexey Panteleev

Posted on

MS Dynamics Web API Quirks: The Strange Case of Polymorphic Fields

If you’ve ever integrated with Microsoft Dynamics 365 / Dataverse, you probably noticed something odd almost immediately. You read a record and see fields like this:

{
  "_ownerid_value": "151F639c-1c73-eb11-b1ab-000d3a253b40"
}

Enter fullscreen mode Exit fullscreen mode

But when creating or updating records, the API suddenly expects something like:

{
  "ownerid@odata.bind": "/systemusers(151F639c-1c73-eb11-b1ab-000d3a253b40)"
}

Enter fullscreen mode Exit fullscreen mode

And if the field is polymorphic, sometimes the property name changes again:

{
  "parentcustomerid_account@odata.bind": "/accounts(ce9eaaef-f718-ed11-b83e-00224837179f)"
}

Enter fullscreen mode Exit fullscreen mode

Welcome to one of the most confusing parts of the Dynamics Web API: lookup and polymorphic fields. Let’s unpack what’s going on.

What are polymorphic fields in Dynamics?

Some relationships in Dynamics can point to multiple entity types.

Example metadata:

{
  "LogicalName": "ownerid",
  "Targets": [
    "systemuser",
    "team"
  ]
}

Enter fullscreen mode Exit fullscreen mode

This means a record owner can be:

  • A system user
  • A team

Other examples of polymorphic lookups include:

Field Possible Targets
ownerid systemuser, team
customerid account, contact
regardingobjectid Many entities

Conceptually this is powerful: the schema allows one relationship field to point to multiple tables. But it introduces some interesting API behavior.


The 3 different names for the same field

One of the first confusing things developers notice is that the same lookup field has different names depending on context.

1️⃣ Metadata name

In the schema or metadata: ownerid

2️⃣ Reading records

When retrieving records, the field becomes: _<field>_value.
Example response:

{
  "_ownerid_value": "151F639c-1c73-eb11-b1ab-000d3a253b40"
}

Enter fullscreen mode Exit fullscreen mode

Additional annotations often appear:

{
  "_ownerid_value": "GUID",
  "_ownerid_value@Microsoft.Dynamics.CRM.lookuplogicalname": "systemuser",
  "_ownerid_value@OData.Community.Display.V1.FormattedValue": "John Smith"
}

Enter fullscreen mode Exit fullscreen mode

Meaning:
| Property | Meaning |
| :--- | :--- |
| _ownerid_value | GUID |
| lookuplogicalname | Entity type |
| FormattedValue | Display value |

So when reading data, you need to interpret three separate pieces of information to understand the relationship.

3️⃣ Writing records

When creating or updating records, Dynamics expects OData binding: <lookup>@odata.bind.

Example:

{
  "ownerid@odata.bind": "/systemusers(151F639c-1c73-eb11-b1ab-000d3a253b40)"
}

Enter fullscreen mode Exit fullscreen mode

Pattern:
<lookup>@odata.bind : "/<entityset>(GUID)"
(Where the entity set name is pluralized).


When polymorphic fields get even stranger

Some polymorphic lookups require the entity name to be embedded in the property.

Examples:

  • "parentcustomerid_account@odata.bind": "/accounts(GUID)"
  • "parentcustomerid_contact@odata.bind": "/contacts(GUID)"

Pattern:
<lookup>_<entity>@odata.bind

This is where things become particularly confusing because the property name itself changes depending on the target entity.

The special case: ownerid

ownerid behaves slightly differently. Unlike other polymorphic fields, you do not include the entity suffix. Both of these work:

  • "ownerid@odata.bind": "/systemusers(GUID)"
  • "ownerid@odata.bind": "/teams(GUID)"

The entity type is inferred from the entity set in the URL, not the property name. This exception is one of the reasons developers often find Dynamics integrations unpredictable.


What developers say about this

If you search StackOverflow or Dynamics forums, you'll see the same questions again and again:

  • Why does the API return _ownerid_value instead of ownerid?
  • Why do some lookups require @odata.bind while others require field_entity@odata.bind?
  • Why does the field name change depending on whether you're reading or writing?

A common pattern in discussions is developers discovering the correct format through trial and error rather than documentation. The API reflects years of evolving platform architecture, and that complexity leaks into the integration layer.


Why this matters for integrations

If you're building integrations with Dynamics — especially sync engines, SaaS connectors, or data pipelines — this complexity adds up quickly. Your integration code ends up handling:

  1. Polymorphic lookup detection
  2. Entity-type resolution
  3. OData binding formats
  4. Multiple naming conventions
  5. Response annotations

A simpler approach: Canonical CRM models

At Aurinko, we’re currently finalizing MS Dynamics support in our canonical CRM API. One of the main goals is to remove these platform-specific quirks from the developer experience.

Instead of dealing with _ownerid_value or ownerid@odata.bind, developers simply work with:

  • owner.id
  • owner.type
  • owner.name

Aurinko handles the heavy lifting behind the scenes:

  • Polymorphic lookup resolution
  • Entity type detection
  • OData binding logic
  • Dynamics naming conventions
  • Schema inconsistencies

The result is a clean, consistent model across CRM platforms, so your integration code doesn't need to understand the quirks of each individual API.

Final thoughts

Microsoft Dynamics is an incredibly capable platform, but its API reflects the complexity of a large enterprise system.

Context Field Name
Metadata ownerid
Read _ownerid_value
Write ownerid@odata.bind

Add polymorphic suffix rules and annotations, and it's easy to see why developers often spend time decoding the API instead of building features. Our goal with Aurinko is simple: make CRM integrations feel predictable again.

And sometimes that starts by hiding _ownerid_value forever.

Top comments (0)