---
title: How to Map Standard Fields to Custom Fields Dynamically in a Unified API
slug: how-to-map-standard-fields-to-custom-fields-dynamically-in-a-unified-api
date: 2026-07-15
author: Uday Gajavalli
categories: [Engineering, Guides]
excerpt: Learn how to map standard fields to custom fields dynamically in a unified API using declarative JSONata expressions and a 3-level override hierarchy.
tldr: Standard unified APIs drop enterprise custom fields. Solve this by using declarative JSONata mappings and a platform-environment-account override hierarchy to dynamically translate schemas per tenant.
canonical: https://truto.one/blog/how-to-map-standard-fields-to-custom-fields-dynamically-in-a-unified-api/
---

# How to Map Standard Fields to Custom Fields Dynamically in a Unified API


Your unified API just dropped 147 custom Salesforce fields into `first_name`, `last_name`, and `email`. The demo went perfectly. The technical evaluation for a six-figure deal, however, was a complete disaster.

B2B SaaS engineering teams inevitably hit a wall when moving upmarket: enterprise customers heavily mutate their CRM and HRIS schemas. A technical evaluation goes smoothly until the prospect's Salesforce administrator hands over their organization's schema. It contains dozens of custom fields on the Contact object, a bespoke `Deal_Registration__c` object with nested relationships, and a `Revenue_Forecast__c` rollup field that drives their entire quarterly planning process.

This is the question every B2B SaaS engineering lead eventually asks: **how do you map standard fields to custom fields dynamically in a unified API without writing bespoke passthrough code for every enterprise tenant?** 

The answer is fundamentally architectural. If your integration infrastructure relies on a rigid, lowest-common-denominator data model, it will drop this critical bespoke data. Traditional unified APIs bake integration logic into code paths (e.g., `if (provider === 'salesforce')`), which forces every custom field into either a rigid schema or a raw passthrough endpoint. You are then forced to abandon your unified abstraction and write raw, integration-specific code just to close the deal.

This guide explains how to map standard fields to custom fields dynamically using declarative transformation languages and hierarchical overrides, answering the fundamental question of [how unified APIs handle custom fields](https://truto.one/how-do-unified-apis-handle-custom-fields-2026-architecture-guide/) at scale. By treating integration logic as data rather than code, you can handle infinite enterprise schema variations from a single generic runtime, supporting 500 tenants with 500 different Salesforce schemas without maintaining a museum of provider-specific backend scripts.

## The Enterprise Integration Reality: Why Standard Data Models Fail

**Custom fields and custom objects are not edge cases. They are the default state of every enterprise SaaS deployment.**

Walk into any Salesforce org above 50 seats and you will find custom fields suffixed with `__c`, custom objects like `Deal_Registration__c`, and rollup summary fields the CFO watches every quarter. Whenever a user creates a custom field in Salesforce, the system automatically appends `__c` at the end of the field name. This suffix is required when writing SOQL queries or using API integrations, as it ensures the system correctly identifies custom fields versus standard objects.

Similarly, platforms like HubSpot gate custom objects behind Enterprise-tier subscriptions. This means every customer paying for that tier is actively using them to map to their unique revenue operations. NetSuite exposes custom segments, custom lists, and SuiteScript-generated fields that vary wildly per subsidiary.

Traditional unified APIs are designed to abstract away the differences between third-party systems by forcing disparate data structures into a single, standardized schema. If Salesforce calls it `FirstName` and HubSpot calls it `firstname`, the unified API normalizes it to `first_name`. 

This "unified" schema that only surfaces `first_name`, `last_name`, `email`, and `phone` is not truly unified—it is simply the intersection of what every vendor offers. That intersection is small. For an enterprise buyer, it is useless.

When applied to an enterprise schema, standard data models fail catastrophically. The unified API evaluates the payload, maps the five fields it recognizes, and silently discards the 73 custom `__c` fields that the enterprise actually cares about. This is exactly [why unified data models break on custom Salesforce objects](https://truto.one/why-unified-data-models-break-on-custom-salesforce-objects-and-how-jsonata-transformations-solve-it/). Data mapping issues and API limitations are consistently cited as leading causes of integration project failure, and rigid schemas are the specific class of mapping problem that kills upmarket deals.

There is also a subtler failure mode. When engineering teams design a lowest-common-denominator object, they hide provider capabilities that the downstream application actually needs. A normalized `Contact` that flattens six phone number types into one loses the semantic distinction between `MobilePhone` and `AssistantPhone`. That distinction matters when the customer's routing engine depends on it.

The correct architectural stance is to separate the normalized model from provider-specific capabilities, giving both first-class support. The unified schema should be the *default view*, not the *only view*, which is the core philosophy behind [unified APIs that don't force standardized data models](https://truto.one/unified-apis-that-dont-force-standardized-data-models/).

## The Problem with Passthrough APIs and Code-First Workarounds

**Passthrough APIs and code-first integration platforms force engineering teams to write and maintain provider-specific logic, defeating the architectural purpose of a unified abstraction.**

When the common model breaks and drops a custom field, unified API vendors typically point you to one of two workarounds. Both introduce severe technical debt and scale poorly.

### Workaround 1: The Passthrough API Lie

Most unified API providers offer a "passthrough" endpoint. "Just call the raw Salesforce API through our proxy," they say, "we handle auth and pagination."

This is the exact moment the abstraction dies.

If you use the passthrough endpoint to query Salesforce custom fields, your application code must now construct raw SOQL queries. You must handle Salesforce's specific pagination cursors. You must parse Salesforce's specific error formats. A passthrough endpoint means you are back to:

- Reading the Salesforce SOQL reference for `WHERE` clause syntax
- Handling `filterGroups` and `CONTAINS_TOKEN` operators for HubSpot search
- Parsing PascalCase versus camelCase field naming per provider
- Writing per-tenant field mapping logic in your application code
- Maintaining that logic when the provider deprecates a field

Suddenly, your codebase is littered with conditional logic. You paid for a unified API, but you are now writing SOQL inside `if (customer_id === 'acme')` branches, because Acme's admin created `Revenue_Forecast__c` but Beta Corp's admin created `ARR_Projection__c` for the exact same concept:

```typescript
// The exact tech debt unified APIs are supposed to prevent
if (integration.provider === 'salesforce') {
  const soql = `SELECT Id, FirstName, Revenue_Forecast__c FROM Contact WHERE AccountId = '${accountId}'`;
  return await passthroughClient.post('/query', { q: soql });
} else if (integration.provider === 'hubspot') {
  // Handle completely different HubSpot filterGroups logic
}
```

### Workaround 2: Code-First Sync Scripts

Some integration platforms advocate for a "code-first" approach, arguing that unified APIs are inherently flawed for the enterprise. Instead of a common model, they ship a TypeScript or Python SDK where you write custom sync scripts for every integration.

While this provides flexibility, it has the same fundamental problem in a different wrapper. You still maintain integration-specific code; it just lives in their sandbox instead of yours. When you write custom code for every integration, your codebase becomes a museum of provider-specific assumptions. 

Adding a new integration requires writing new handler functions, creating new database schemas, writing integration-specific tests, and executing a full CI/CD deployment. When you have 40 integrations and 500 enterprise tenants, you have 20,000 potential code branches. Nobody staffs an integration team large enough to review that.

The root cause in both cases is identical: **integration-specific behavior is expressed as code**. Every new field, every new tenant customization, and every provider API change requires a CI/CD cycle. Code-first platforms scale linearly with the number of integrations times the number of customer variations. That is a hiring plan, not an architecture.

## How Declarative Mapping Solves the Custom Field Problem

**Declarative mapping replaces rigid schemas with functional transformation languages, allowing dynamic translation of API requests and responses at runtime without writing backend code.**

To support enterprise custom fields without writing integration-specific code, the architecture must separate the normalized interface from the provider's specific constraints. This is achieved using the **Interpreter Pattern** at a platform scale.

A declarative mapping architecture treats every field translation as a pure function from a provider response to a unified shape. Instead of writing TypeScript handlers for each integration, integration behavior is defined entirely as data. A generic execution engine takes a declarative configuration describing how to talk to a third-party API, and a declarative mapping describing how to translate between unified and native formats.

### JSONata: The Universal Transformation Engine

Modern integration platforms rely on JSONata as their transformation engine. JSONata is a Turing-complete functional query and transformation language purpose-built for reshaping JSON objects.

Because JSONata expressions are pure functions (they transform input to output without modifying state) and are stored as simple strings, they can be saved in a database column, versioned, overridden, and hot-swapped without restarting the application or touching source code.

> [!NOTE]
> **Why JSONata?** Unlike basic key-value mappers found in legacy iPaaS tools, JSONata supports conditionals, string manipulation, array transforms, date formatting, custom functions, and recursive expressions. It is powerful enough to handle dynamic SOQL query generation, conditional routing, array folding, and regex-driven custom field detection.

### The Generic Execution Pipeline

When a request enters the system, the execution engine follows a strict, integration-agnostic pipeline. 

```mermaid
flowchart TD
    A["Unified Request<br>GET /crm/contacts"] --> B["Resolve Mapping Config<br>From Database"]
    B --> C["Extract JSONata Expressions"]
    C --> D["Evaluate Request Mapping<br>Transform Query/Body"]
    D --> E["Proxy Layer<br>Execute HTTP Call"]
    E --> F["Evaluate Response Mapping<br>Normalize Payload"]
    F --> G["Return Unified Data"]
```

Notice that nowhere in this pipeline does the engine check if the provider is Salesforce or HubSpot. It simply evaluates the JSONata expression provided by the configuration.

Here is what a declarative JSONata response mapping for Salesforce contacts looks like as pure data:

```jsonata
response.{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "name": $join($removeEmptyItems([FirstName, LastName]), " "),
  "title": Title,
  "account": { "id": AccountId },
  "email_addresses": [{ "email": Email }],
  "phone_numbers": $filter([
    { "number": Phone, "type": "phone" },
    { "number": MobilePhone, "type": "mobile" },
    { "number": AssistantPhone, "type": "assistant" }
  ], function($v) { $v.number }),
  "custom_fields": $sift($, function($v, $k) {
    $k ~> /__c$/i and $boolean($v)
  })
}
```

Read the last block carefully. The `$sift` function walks every key on the raw response, keeps the ones matching the `__c` suffix pattern, and hands them back as a `custom_fields` object. That single expression handles every custom field in every Salesforce org, forever. When a customer adds `Revenue_Forecast__c` in production tomorrow, it appears in `custom_fields` on the next API call. No code change. No deploy. No release notes. The underlying execution engine never needs to know what `__c` means.

The same pattern works for HubSpot, which stores custom fields inside a `properties` object:

```jsonata
(
  $defaults := ["firstname", "lastname", "email", "phone"];
  $diff := $difference($keys(response.properties), $defaults);
  {
    "id": response.id,
    "first_name": response.properties.firstname,
    "email_addresses": [{ "email": response.properties.email }],
    "custom_fields": response.properties.$sift(function($v, $k) { $k in $diff })
  }
)
```

The *runtime* that evaluates these expressions has no idea it is talking to Salesforce or HubSpot. It loads the expression string, evaluates it against the raw response, and returns the result. The same code path serves both providers.

For a deeper dive into schema normalization techniques, read our [API Schema Normalization Tutorial: End-to-End with JSONata](https://truto.one/developer-tutorial-end-to-end-api-schema-normalization-with-jsonata/).

## The 3-Level Override Hierarchy: Platform, Environment, and Account

**A multi-level override hierarchy allows engineering teams to customize API mappings at the platform level for all users, the environment level for specific deployments, or the account level for individual enterprise tenants.**

Declarative mappings solve the *default* case, but dynamic mapping is only half the solution. They do not solve the *bespoke tenant* case, where Acme Corp wants their highly mutated `Revenue_Forecast__c` promoted to a first-class unified field, while Beta Corp wants the same field mapped to a differently named property.

If an enterprise customer has a highly mutated CRM schema, you need a way to [configure field mappings per customer without forking code](https://truto.one/what-integration-platform-lets-me-configure-field-mappings-per-customer-without-forking-code/) or affecting the rest of your user base. This is solved by implementing a three-level override hierarchy. The configuration system deep-merges mapping rules at request time.

### Level 1: The Platform Base

The base mapping is the default configuration shipped with the integration. It translates standard fields like `first_name` and `email` between the unified model and the provider's native API. You author this once, test it against a live provider account, and it works for 80% of customers. This mapping is shared across all environments.

### Level 2: Environment Override

Every SaaS application has one or more environments (dev, staging, prod, or per-workspace). An environment can override any mapping key for every integration used inside it. For example, a staging environment might need to map data to a sandbox-specific custom object, or a GRC vendor might extend the unified `users` mapping to always surface an `mfa_enabled` field pulled from the raw payload, applying that rule across every one of their tenants.

### Level 3: Integrated Account Override

This is the critical layer for enterprise sales. Individual connected accounts—one specific customer's Salesforce org—can have their own mapping overrides attached to their specific tenant record. 

If Acme Corp requires their `Revenue_Forecast__c` field to be mapped to a custom `projected_revenue` field in your application, you simply inject a JSONata override onto Acme Corp's integrated account record:

```json
{
  "response_mapping": "{ 'projected_revenue': Revenue_Forecast__c }"
}
```

At runtime, the resolution is straightforward. The engine loads the base mapping, deep-merges the environment override, and then deep-merges the account override per mapping key. The merged expression is compiled and evaluated:

```typescript
// Pseudocode - the actual merge happens per mapping key
const effective = deepMerge(
  platformBase,        // Level 1
  environmentOverride, // Level 2
  accountOverride      // Level 3
)

const mapped = await jsonata(effective.response_mapping)
  .evaluate({ response, query, context, headers })
```

Acme Corp gets their custom field exactly where they want it, and your engineering team didn't have to write a single line of custom TypeScript. Every other tenant continues using the default. 

What this enables in practice:
- A customer can add their own custom fields to the unified response without you shipping code.
- A customer can rewrite filter behavior for their specific setup.
- You can support conflicting tenant requirements without forking.
- Rollback is a config revert, not a hotfix deploy.

### Architecture at a Glance

```mermaid
flowchart LR
    A[Unified API Request] --> B[Generic Runtime]
    B --> C[Load Platform Base Mapping]
    B --> D[Load Environment Override]
    B --> E[Load Account Override]
    C --> F[Deep Merge]
    D --> F
    E --> F
    F --> G[Evaluate JSONata Expression]
    G --> H[Call Third-Party API]
    H --> I[Evaluate Response JSONata]
    I --> J[Unified Response + remote_data]
```

Notice what is not in that diagram: a switch statement on the provider name. There is no `SalesforceHandler` class. The runtime is generic. The behavior is data.

For more details on implementing this architecture, see [Per-Customer API Mappings: 3-Level Overrides for Enterprise SaaS](https://truto.one/per-customer-api-mappings-3-level-overrides-for-enterprise-saas/).

## Handling Rate Limits and API Errors During Dynamic Mapping

**When performing dynamic field mapping, the integration infrastructure must transparently pass upstream HTTP 429 rate limits to the caller using normalized IETF headers, leaving retry and backoff logic to the consuming application.**

A common architectural mistake in unified APIs is attempting to abstract away rate limits. Many platforms intercept HTTP 429 (Too Many Requests) errors and apply automatic exponential backoff under the hood. 

This is a dangerous anti-pattern. Black-box retries cause unpredictable latency spikes, hold open network connections, amplify thundering herds, and hide architectural bottlenecks from the consuming application. If an AI agent or a high-volume sync job hits a rate limit, the caller needs to know immediately so it can pause its internal queues or circuit breakers. Explicit failure gives the caller a decision point.

Modern integration layers take a radically transparent approach to rate limits. The platform does not silently retry, throttle, or absorb the error. Instead, when an upstream API returns an HTTP 429, that error is passed straight through to the caller. 

However, because every third-party API formats its rate limit data differently (Salesforce uses one shape, HubSpot another, NetSuite governance limits live in a completely different response envelope), the mapping layer collapses all provider-specific rate limit metadata into standardized IETF headers before passing the response back:

- `ratelimit-limit`: The total request quota.
- `ratelimit-remaining`: The number of requests remaining in the current window.
- `ratelimit-reset`: The timestamp when the quota resets.

By normalizing the headers but passing the error, the integration layer remains stateless and predictable. The consuming application can accurately schedule its retry logic by reading one contract instead of fifteen.

Additionally, for field-level errors during mapping (e.g., a JSONata expression referencing a field the provider stopped returning), the platform should surface the raw error and preserve the original, unmapped response as `remote_data` on every unified object. This ensures debugging never requires reproducing the failing request against the provider directly, and gives callers an escape hatch for fields not covered by the unified schema.

## Shipping Integrations as Data, Not Code

**By abstracting integration logic into declarative configuration data, product teams can ship new connectors, support custom enterprise fields, and handle API changes without triggering CI/CD pipelines or deploying backend code.**

The custom fields problem is fundamentally an architecture problem. The reason this architecture matters is operational, not aesthetic.

When integration behavior is code, every change is a deploy. Every deploy has a blast radius. Every blast radius requires review, CI runs, canary rollout, and rollback plans. A code-per-integration platform scales its release process linearly with the number of connectors it supports. Your maintenance burden will grow linearly with every enterprise customer you sign.

When integration behavior is data, adding a new provider is a JSON write. Adding a new custom field mapping for one tenant is a database update. Adding a new unified resource across every existing integration is a set of JSONata expressions authored against live provider responses. The generic runtime that handles 100 integrations today will handle the 101st without a single line of code being changed, compiled, or deployed.

This is what makes per-tenant customization economically viable. You can afford to accommodate Acme's schema quirks because accommodating them costs a config change, not an engineer-week. You can afford to onboard the enterprise deal that requires three custom fields promoted into the unified schema, because promoting them costs a mapping edit reviewable in a simple pull request.

The trade-offs are real and worth acknowledging. Declarative expressions are harder to grep than TypeScript. Debugging a JSONata expression that returns `null` requires a REPL and a sample payload. Complex conditional logic pushes the readability limit of any expression language. Good tooling—a validator, a sample-based mapping editor, and thorough test coverage against recorded provider responses—is what makes this architecture livable in production.

The payoff is that integration engineering stops looking like feature engineering and starts looking like content authoring. That is the right shape for a problem where the surface area is dictated by 200 vendors and 5,000 tenant variations, not by your own product roadmap.

## Next Steps for Integration Architects

If you are picking apart your current integration architecture and want to move toward a declarative model to handle infinite enterprise schema variations, three concrete moves pay off first:

1. **Audit where integration-specific code lives.** Grep your codebase for provider names in conditionals (`if provider === 'salesforce'`). Every match is a place where a config change would beat a code change.
2. **Model the override hierarchy explicitly.** Even if you build it yourself, decide up front whether customization happens at the platform, environment, or account level. Mixing them without a clear precedence rule is worse than not supporting overrides at all.
3. **Preserve the raw provider payload on every unified object.** A `remote_data` field is the escape hatch that saves the abstraction. Callers can reach into it when the unified schema does not cover something, without you needing to expose a passthrough endpoint.

> Stop losing enterprise deals to rigid data models. Bring your gnarliest custom object requirement, and we will show you the JSONata expression that handles it in the demo call.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
