Skip to content

Developer Guide: JSONata Mapping Examples for API Integration (2026)

Learn how to replace brittle integration code with declarative JSONata expressions. Includes concrete mapping examples for custom objects, arrays, and errors.

Uday Gajavalli Uday Gajavalli · · 11 min read
Developer Guide: JSONata Mapping Examples for API Integration (2026)

If you need to normalize API schemas across dozens of SaaS providers without writing a dedicated Python or JavaScript adapter for each one, you need a declarative transformation layer. If you're searching for JSONata mapping examples to normalize API data, you're probably maintaining a growing pile of per-provider transformation scripts and feeling the pain.

This guide provides concrete, working code samples you can adapt today—extracting nested fields, mapping polymorphic arrays, handling conditional custom fields, and normalizing error responses—all with declarative JSONata expressions instead of brittle, hardcoded scripts.

Building a single API connector is straightforward. Maintaining 50 of them is an engineering nightmare. Every time a vendor deprecates an endpoint, changes a pagination strategy, or introduces a new custom object, your team has to open the codebase, update the specific API adapter, write new tests, and deploy a fix.

By treating API integration as a data transformation problem rather than a software engineering problem, you can ship connectors faster, eliminate integration-specific code, and empower your organization to handle custom enterprise requirements on the fly.

Why Code-First API Mapping Fails at Scale

Most engineering teams start building integrations using the strategy pattern. They define a common interface in their application and write a separate adapter class for every third-party API. Every B2B SaaS integration starts innocently. You write a mapHubSpotContact() function. It works. Then you add Salesforce, and you write mapSalesforceContact(). Then Pipedrive, Zoho, Close, Dynamics 365. Each function handles that provider's specific field names, nesting patterns, date formats, and phone number layouts.

Behind the scenes, this architecture relies on brute force. The codebase becomes littered with conditional logic: if (provider === 'hubspot') { ... } else if (provider === 'salesforce') { ... }. You end up maintaining integration-specific database columns, dedicated handler functions, and hardcoded business logic that must be updated every time an upstream API changes.

Within two years, you're maintaining dozens of transformation scripts with nearly identical logic but completely different implementations. Industry research indicates that API integrations can range from $2,000 for simple setups to more than $30,000, with ongoing annual costs of $50,000 to $150,000 for staffing and maintenance. Most of that ongoing cost is not building new features. It's updating existing mapping code when vendors alter their response payloads.

The code-first approach breaks in three specific ways:

  • Linear maintenance burden: Each new integration adds a new code path that must be independently tested, reviewed, and deployed. Bug fixes don't propagate across providers.
  • Deployment coupling: A mapping change for one provider requires a full CI/CD cycle, even if the fix is simply changing properties.firstname to properties.first_name.
  • No customization path for customers: When an enterprise prospect has 147 custom fields on their Salesforce Contact object, your engineering team must write per-customer code. That doesn't scale past a handful of accounts.

This is why schema normalization is the hardest problem in SaaS integrations—and it's the single biggest bottleneck for engineering teams. The moment you deploy your code, an enterprise customer will connect a Salesforce instance containing custom fields that your static types do not understand.

What Is JSONata? The Declarative Alternative

To escape the maintenance trap of code-first integrations, modern architectures use an interpreter pattern. Instead of writing code to translate data, you write a configuration that describes the translation, and a generic engine executes it.

JSONata is a functional query and transformation language designed specifically for JSON data.

Created by Andrew Coleman and his colleagues at IBM in 2016, JSONata is an open-source query language inspired by the location path semantics of XPath 3.1. It lets you express complex data reshaping in compact, readable expressions rather than procedural code. Think of it as a Turing-complete expression language purpose-built for reshaping JSON objects.

Instead of writing 20 lines of JavaScript with .map(), .filter(), optional chaining, and null checks, you write a single JSONata expression that declares the shape of the output. The runtime handles traversal, null safety, and type coercion. Unlike jq, which is primarily designed for filtering data in command-line environments, JSONata is designed to construct entirely new JSON structures.

It is widely adopted in enterprise orchestration and IoT platforms. IBM App Connect Enterprise uses a JSONata Mapping node in message flows to build messages in JSON format. Node-RED, a flow-based low-code development tool originally developed by IBM, embeds JSONata natively for data transformation. Orchestration platforms like Kestra call JSONata the "Swiss Army Knife" for JSON transformation, and Blues Wireless embeds it to manipulate data in real-time edge computing workflows.

The key properties that make JSONata ideal for API integration include:

Property Why It Matters for Integration
Declarative Describe the output shape, not the step-by-step procedure to build it.
Null-safe Missing fields return undefined silently without throwing null reference errors or crashing the application.
Turing-complete Supports conditionals, recursion, custom functions, and lambdas.
Side-effect free Expressions are pure functions. They transform input to output without modifying external application state.
Storable as data An expression is just a string. It can be stored in a database column, versioned, overridden, and hot-swapped without restarting the application.

Because a JSONata expression is just a string, it can be stored as configuration data rather than compiled code. This opens the door to hot-swappable API integrations that do not require code deploys.

JSONata Mapping Examples for API Integration

The following JSONata mapping examples demonstrate how to solve the most common and painful API integration challenges without writing backend code. All examples can be tested in the JSONata Exerciser online.

Example 1: Extracting and Flattening Nested CRM Contact Fields

Many APIs nest core data inside arbitrary wrapper objects. HubSpot, for instance, places contact data inside a properties object. If you want to map this into a clean, flat unified schema, JSONata handles it natively.

HubSpot API response (input):

{
  "id": "501",
  "properties": {
    "firstname": "Jane",
    "lastname": "Martinez",
    "jobtitle": "VP Engineering",
    "email": "jane@acme.com",
    "phone": "+1-555-0199",
    "mobilephone": "+1-555-0200"
  },
  "createdAt": "2024-03-15T10:30:00Z",
  "updatedAt": "2025-11-20T14:15:00Z"
}

JSONata expression:

{
  "id": id,
  "first_name": properties.firstname,
  "last_name": properties.lastname,
  "title": properties.jobtitle,
  "email_addresses": [
    properties.email ? {"email": properties.email, "is_primary": true}
  ],
  "phone_numbers": [
    properties.phone ? {"number": properties.phone, "type": "work"},
    properties.mobilephone ? {"number": properties.mobilephone, "type": "mobile"}
  ],
  "created_at": createdAt,
  "updated_at": updatedAt
}

Output:

{
  "id": "501",
  "first_name": "Jane",
  "last_name": "Martinez",
  "title": "VP Engineering",
  "email_addresses": [{"email": "jane@acme.com", "is_primary": true}],
  "phone_numbers": [
    {"number": "+1-555-0199", "type": "work"},
    {"number": "+1-555-0200", "type": "mobile"}
  ],
  "created_at": "2024-03-15T10:30:00Z",
  "updated_at": "2025-11-20T14:15:00Z"
}

Notice the ternary pattern: properties.phone ? {"number": properties.phone, "type": "work"}. If phone is absent, the entire object is omitted from the array. No if statements, no null checks, and no application crashes.

Example 2: Mapping Polymorphic Arrays (Multiple Phone Number Types)

Salesforce takes the opposite approach to HubSpot. Instead of a nested object, it returns flat PascalCase fields. Worse, it returns up to six separate phone fields (Phone, MobilePhone, Fax, etc.). You need them in a single phone_numbers array, but only the ones that actually have values.

Salesforce API response (input):

{
  "Id": "003xx000004TmiY",
  "FirstName": "John",
  "LastName": "Doe",
  "Phone": "+1-555-0101",
  "Fax": null,
  "MobilePhone": "+1-555-0102",
  "HomePhone": null,
  "OtherPhone": "+1-555-0103",
  "AssistantPhone": null
}

JSONata expression:

{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "name": $join([FirstName, LastName], " "),
  "phone_numbers": $filter([
    {"number": Phone, "type": "work"},
    {"number": Fax, "type": "fax"},
    {"number": MobilePhone, "type": "mobile"},
    {"number": HomePhone, "type": "home"},
    {"number": OtherPhone, "type": "other"},
    {"number": AssistantPhone, "type": "assistant"}
  ], function($v) { $v.number })
}

The $filter function with a predicate function($v) { $v.number } drops any entry where number is null or undefined. This handles the variability of Salesforce's six phone fields in a single expression—no loops, no conditionals.

Example 3: Conditional Custom Field Detection

Enterprise software is highly customized. Your unified schema might cover 20 standard fields, but the customer's Salesforce instance has 50 custom fields ending in __c. You need to capture these dynamically without knowing them in advance.

JSONata expression:

{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "standard_fields": {"email": Email, "phone": Phone},
  "custom_fields": $sift($, function($v, $k) {
    $k ~> /__c$/i and $boolean($v)
  })
}

Given an input with arbitrary custom fields, the $sift function scans every key in the root object ($). The regex /__c$/i matches Salesforce custom field names. The $boolean($v) check skips null or empty values. All matching key-value pairs are swept into the custom_fields object dynamically.

For APIs like HubSpot that don't use a specific suffix, you can use the $difference function to subtract your known default fields from the total list of keys, placing whatever remains into the custom fields object. This is the exact mechanism that unlocks mapping custom objects without per-customer code.

Example 4: Enum Normalization Across Providers

Different providers use different strings for the same concept. Mapping enums declaratively is where JSONata really shines over handwritten switch statements.

JSONata expression:

(
  $statusMap := {
    "ACTIVE": "active",
    "Active": "active",
    "active": "active",
    "INACTIVE": "inactive",
    "Inactive": "inactive",
    "Disabled": "inactive",
    "SUSPENDED": "suspended",
    "On Leave": "on_leave",
    "ON_LEAVE": "on_leave"
  };
 
  {
    "id": employee.id,
    "name": employee.display_name,
    "status": $lookup($statusMap, employee.status)
  }
)

The lookup table is data, not logic. When a new HRIS provider uses "TERMINATED" instead of "INACTIVE", you add one line to the map object. No code change, no deployment.

Example 5: Remapping HTTP Status Codes and Errors

APIs return errors in wildly different formats. Slack, for example, returns a 200 OK HTTP status for almost everything, signaling errors inside the JSON body ({"ok": false, "error": "invalid_auth"}). If your system relies on HTTP status codes, Slack errors will silently pass through as successes.

We can use JSONata to inspect the response body and remap the HTTP status code before it reaches the application logic.

JSONata expression (Error Mapping):

$not(data.ok) ? {
  "status": $lookup({
    "invalid_auth": 401,
    "token_expired": 401,
    "missing_scope": 403,
    "ratelimited": 429,
    "channel_not_found": 404,
    "internal_error": 500
  }, data.error),
  "message": data.error
}

This expression only fires when data.ok is false. The expression uses $lookup to map the specific Slack error string to the correct standard HTTP status code. If data.ok is true, the ternary evaluates to undefined, and the system falls through to normal processing.

Handling Edge Cases: Pagination, Errors, and Rate Limits

Mapping response fields is only half the integration story. When you move integration logic to a declarative configuration, you must also handle the operational realities of interacting with third-party APIs.

Rate Limit Transparency

Third-party rate limits are notoriously inconsistent. HubSpot uses X-HubSpot-RateLimit-Daily-Remaining. Salesforce uses custom headers. Some APIs don't expose rate limit info at all. A major interoperability issue in throttling is the lack of standard headers.

The IETF's draft-ietf-httpapi-ratelimit-headers specification defines standardized ratelimit-limit, ratelimit-remaining, and ratelimit-reset header fields to address this fragmentation. When building a unified API engine, it is critical to normalize this information.

At Truto, the engine extracts the provider-specific data and normalizes it into these standard IETF headers. When an upstream API returns an HTTP 429 error, the platform passes the 429 error directly to the caller alongside the normalized headers. This ensures the calling application maintains complete control over its own retry and backoff logic, rather than having the integration layer silently absorb and block requests.

Pagination Strategy as Configuration

Different APIs paginate differently—cursor-based, page-number, offset, Link headers. In a code-first approach, each pagination style needs its own implementation per provider. With a declarative model, the pagination strategy itself becomes a configuration field:

{
  "pagination": {
    "format": "cursor",
    "config": {
      "cursor_field": "paging.next.after",
      "cursor_param": "after"
    }
  }
}

The generic execution engine reads this config and applies the right strategy, completely eliminating per-integration pagination loops.

Multi-Step Orchestration

Sometimes a single unified request requires multiple API calls. For example, creating a contact and immediately associating it with a company. Declarative configurations handle this using pre-request and post-request step arrays. A before step can execute a JSONata expression to fetch a list of custom fields, store them in state, and then execute the main request. An after step can take the newly created entity ID and make a subsequent API call to establish a relationship. All of this orchestration happens in the generic execution pipeline without custom backend code.

Moving from Code to Configuration: The Architectural Shift

The pattern across all these examples is the same: move integration logic from compiled code to declarative data. JSONata expressions are strings. Strings can be stored in a database. Database records can be updated without a deployment.

flowchart LR
    A[Unified API Request] --> B[Generic Engine]
    B --> C{Read Config}
    C --> D[JSONata Query Mapping]
    C --> E[JSONata Response Mapping]
    C --> F[Pagination Strategy]
    D --> G[Third-Party API Call]
    G --> E
    E --> H[Normalized Response]

When integration behavior is entirely data-driven, adding a new connector is a data operation. You insert a JSON configuration describing the API endpoints and a set of JSONata expressions describing the data mapping. The exact same generic execution engine handles a HubSpot CRM contact listing and a Salesforce CRM contact listing. The differences are all captured in JSONata expressions stored as data.

This architecture also enables per-customer customization through a 3-level override hierarchy:

  1. Platform Base: The default JSONata mapping that works for most customers.
  2. Environment Override: Modifications applied to a specific customer environment.
  3. Account Override: Deeply specific tweaks applied to a single integrated account.

If an enterprise customer needs a specific custom field mapped to a non-standard location, a product manager can write a JSONata override for that specific account and save it to the database. The 3-level API mapping system deep-merges the configurations at runtime. The customer gets their custom integration immediately, and the engineering team never touches a pull request.

The Honest Trade-offs

JSONata is not without costs. That elegant syntax and power are expensive in terms of performance. Native JavaScript will always be faster for raw throughput. The richness of JSONata's expression language comes with a steeper learning curve, especially for users unfamiliar with functional programming concepts.

However, for most API integration workloads—mapping individual response objects, transforming query parameters, and normalizing small JSON payloads—the performance overhead is negligible. You are typically transforming single records or small pages, not processing millions of rows concurrently.

Debugging is the other real pain point. JSONata doesn't throw errors for missing fields; it returns undefined. A typo in a field name produces silence, not a stack trace. You must mitigate this by validating incoming data schemas and extensively unit-testing every expression.

What This Means for Your Integration Strategy

If your team is still writing if (provider === 'hubspot') branches, you're accumulating technical debt that compounds with every new integration. The shift from imperative mapping code to declarative JSONata expressions isn't just a style preference—it's an architectural decision that determines whether your integration layer scales linearly or exponentially with the number of providers.

Start small. Pick one mapping function in your codebase and rewrite it as a JSONata expression. Store that expression in your database instead of your source code. Measure how long the next mapping change takes: updating a database record versus going through a full PR, CI, and deployment cycle. The results will speak for themselves.

FAQ

What is JSONata and how is it used for API data mapping?
JSONata is a lightweight, functional query and transformation language for JSON data. In API integration, it replaces imperative mapping scripts (JavaScript/Python) with declarative expressions that describe the output shape. Because expressions are strings, they can be stored as configuration and updated without code deployments.
How does JSONata handle null or missing fields in API responses?
JSONata is null-safe by default. If you reference a field that doesn't exist, the expression returns undefined rather than throwing a null reference error. This eliminates the need for optional chaining or conditional null checks.
Can JSONata detect and map Salesforce custom fields dynamically?
Yes. Using JSONata's $sift function with a regex pattern like /__c$/i, you can dynamically extract all custom fields from a Salesforce response without knowing them in advance. This works for any organization regardless of how many custom fields they have created.
Is JSONata fast enough for production API integrations?
For typical API integration workloads (transforming individual records or small pages of results), JSONata's performance overhead is negligible. Native JavaScript is faster for raw throughput on massive datasets, but most API mappings transform small JSON payloads where the difference is immaterial.
How do you handle API rate limits with declarative mapping?
Declarative configurations can extract provider-specific rate limit data and normalize it into standard IETF headers (ratelimit-remaining). Best practice is to pass the HTTP 429 error and normalized headers back to the caller so they control their own retry and backoff logic.

More from our Blog