How to Map Standard Fields to Custom Fields Dynamically in a Unified API
Learn how to map standard fields to custom fields dynamically in a unified API using declarative JSONata expressions and a 3-level override hierarchy.
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 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. 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.
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
WHEREclause syntax - Handling
filterGroupsandCONTAINS_TOKENoperators 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:
// 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.
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.
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:
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:
(
$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.
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 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:
{
"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:
// 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
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.
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:
- 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. - 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.
- Preserve the raw provider payload on every unified object. A
remote_datafield 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.
FAQ
- Why do traditional unified APIs fail at custom field mapping?
- They express integration behavior as code with provider-specific branches, forcing every field into a lowest-common-denominator schema. Custom fields either get dropped or pushed to a passthrough endpoint that requires you to write raw provider-specific queries yourself.
- What is JSONata and why is it used for dynamic field mapping?
- JSONata is a Turing-complete functional query and transformation language for JSON. Because a JSONata mapping is just a string, it can be stored in a database, versioned, and hot-swapped without code deploys, allowing dynamic per-tenant mapping at runtime.
- How does a 3-level mapping override hierarchy work?
- It allows mappings to be defined at the base platform level, customized for specific deployment environments, and overridden for individual integrated accounts. At request time, the three levels are deep-merged per mapping key without modifying the underlying codebase.
- Does dynamic mapping handle upstream API rate limits?
- Yes, but not by absorbing them. The platform passes HTTP 429 errors through to the caller unchanged, while normalizing provider-specific rate limit metadata into standardized IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) so the caller can properly handle retries.
- How do you add a new integration in a declarative architecture?
- You author a JSON config describing the API and a set of JSONata expressions mapping unified resources to the provider's native format. Both are stored in the database. No code deploy is needed because the generic runtime executes the new configuration identically.