Replicating DataWeave Transformations in Kong Gateway Plugins
What MuleSoft made easy, Kong makes possible — just differently.
Replicating DataWeave Transformations in Kong Gateway Plugins
What MuleSoft made easy, Kong makes possible — just differently.
One of the hardest things to give up when leaving MuleSoft isn’t the platform itself. It’s DataWeave.
If you’ve spent time building complex XML/JSON transformations, SOAP envelope handling, or recursive payload masking in DataWeave, you know what I mean. It’s expressive, concise, and purpose-built for the job. When we started migrating APIs to Kong Gateway, the gateway policies were the easy part. The transformation logic was the challenge.
This post covers two of the most common — and most complex — scenarios we had to replicate: XML ↔ JSON conversion and SOAP envelope wrapping/unwrapping. For each one, I’ll show the original DataWeave approach and then the Kong plugin equivalent in JavaScript (using Kong’s lua-resty-core or the JS PDK via kong-js-pdk).
A quick note on Kong plugin options
Kong supports two main paths for custom transformation logic:
- Lua plugins — native to Kong, fast, low overhead. Best for lightweight transformations.
- JavaScript plugins (via
kong-js-pdk) — more familiar syntax for most teams, better for complex logic like XML parsing. Requires the JS plugin server running alongside Kong.
For the scenarios below, we’ll use JavaScript plugins since XML manipulation in Lua is painful. If your team is Lua-comfortable, the logic maps across, but the XML parsing story is much cleaner in JS.
Scenario 1: XML ↔ JSON conversion
The DataWeave version
In MuleSoft, this was almost embarrassingly simple:
dataweave
%dw 2.0
output application/json
---
payload
Going the other direction — JSON to XML — was equally concise:
dataweave
%dw 2.0
output application/xml
---
{
root: payload
}
DataWeave handled type coercion, attribute mapping, and namespace handling automatically. The runtime knew the input type from the Content-Type header and did the rest.
The Kong plugin version
In Kong, we need to intercept the request or response body, parse it, transform it, and rewrite it. Here’s a working JS plugin that converts an XML request body to JSON before it reaches the upstream service:
// kong-xml-to-json-plugin/index.js
const { XMLParser } = require('fast-xml-parser');
class KongXmlToJsonPlugin {
constructor(config) {
this.config = config;
}
async access(kong) {
const contentType = await kong.request.getHeader('content-type') || '';
if (!contentType.includes('application/xml') && !contentType.includes('text/xml')) {
return;
}
const body = await kong.request.getRawBody();
if (!body) return;
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
parseAttributeValue: true,
});
try {
const parsed = parser.parse(body.toString());
const jsonBody = JSON.stringify(parsed);
await kong.service.request.setRawBody(jsonBody);
await kong.service.request.setHeader('content-type', 'application/json');
await kong.service.request.setHeader('content-length', Buffer.byteLength(jsonBody).toString());
} catch (err) {
await kong.response.exit(400, { message: 'Invalid XML payload', detail: err.message });
}
}
}
module.exports = {
Plugin: KongXmlToJsonPlugin,
Schema: [],
Version: '1.0.0',
Priority: 850,
};
For the reverse direction — JSON response converted to XML before returning to the client — you intercept in the response phase:
const { XMLBuilder } = require('fast-xml-parser');
async response(kong) {
const acceptHeader = await kong.request.getHeader('accept') || '';
if (!acceptHeader.includes('application/xml')) return;
const body = await kong.service.response.getRawBody();
if (!body) return;
try {
const parsed = JSON.parse(body.toString());
const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_' });
const xmlBody = builder.build({ root: parsed });
await kong.response.setRawBody(xmlBody);
await kong.response.setHeader('content-type', 'application/xml');
} catch (err) {
// If the response isn't JSON, pass it through unchanged
}
}
What DataWeave handles automatically that you now own:
- Namespace declarations — you’ll need to add these explicitly in the XMLBuilder config if your upstream expects namespaced XML
- Attribute vs element distinction —
fast-xml-parser's@_prefix convention is your friend here - Array coercion — single-element XML nodes don’t automatically become arrays; add
isArrayrules to your parser config for known list fields
Scenario 2: SOAP envelope wrapping and unwrapping
This is where the gap between DataWeave and Kong plugins feels widest. SOAP handling in MuleSoft is first-class. In Kong, you’re building it yourself.
The DataWeave version
A typical SOAP wrapping transformation in DataWeave looked like this:
dataweave
%dw 2.0
output application/xml
ns soap http://schemas.xmlsoap.org/soap/envelope/
ns ns0 http://example.com/service
---
{
soap#Envelope: {
soap#Header: {},
soap#Body: {
ns0#GetCustomerRequest: {
ns0#customerId: payload.customerId,
ns0#locale: payload.locale default "es-ES"
}
}
}
}
Clean, readable, namespace-aware. Unwrapping was just as straightforward — navigate the envelope and extract the body content.
The Kong plugin version
We split this into two plugins: one for wrapping outbound REST→SOAP calls, one for unwrapping SOAP responses back to JSON. Here’s the wrapping plugin:
// kong-soap-wrapper-plugin/index.js
class SoapWrapperPlugin {
constructor(config) {
this.config = config;
this.serviceNamespace = config.service_namespace || 'http://example.com/service';
this.operation = config.operation || 'GetCustomerRequest';
}
async access(kong) {
const body = await kong.request.getRawBody();
if (!body) return;
let payload;
try {
payload = JSON.parse(body.toString());
} catch {
await kong.response.exit(400, { message: 'Expected JSON input for SOAP wrapping' });
return;
}
const fields = Object.entries(payload)
.map(([k, v]) => `<ns0:${k}>${v}</ns0:${k}>`)
.join('\n ');
const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns0="${this.serviceNamespace}">
<soap:Header/>
<soap:Body>
<ns0:${this.operation}>
${fields}
</ns0:${this.operation}>
</soap:Body>
</soap:Envelope>`;
await kong.service.request.setRawBody(soapEnvelope);
await kong.service.request.setHeader('content-type', 'text/xml; charset=utf-8');
await kong.service.request.setHeader('soapaction', `"${this.serviceNamespace}/${this.operation}"`);
await kong.service.request.setHeader('content-length', Buffer.byteLength(soapEnvelope).toString());
}
}
module.exports = {
Plugin: SoapWrapperPlugin,
Schema: [
{ service_namespace: { type: 'string' } },
{ operation: { type: 'string' } },
],
Version: '1.0.0',
Priority: 850,
};
And the unwrapping plugin for the response:
// kong-soap-unwrapper-plugin/index.js
const { XMLParser } = require('fast-xml-parser');
class SoapUnwrapperPlugin {
constructor(config) {
this.config = config;
}
async response(kong) {
const contentType = await kong.service.response.getHeader('content-type') || '';
if (!contentType.includes('xml')) return;
const body = await kong.service.response.getRawBody();
if (!body) return;
const parser = new XMLParser({
ignoreAttributes: true,
removeNSPrefix: true, // strips soap: ns0: prefixes - key for clean output
});
try {
const parsed = parser.parse(body.toString());
// Navigate: Envelope > Body > first child (the operation response)
const envelope = parsed['Envelope'] || parsed['soap:Envelope'];
const soapBody = envelope?.['Body'] || envelope?.['soap:Body'];
const operationResponse = soapBody ? Object.values(soapBody)[0] : null;
if (!operationResponse) {
await kong.response.exit(502, { message: 'Could not parse SOAP body' });
return;
}
const jsonResponse = JSON.stringify(operationResponse);
await kong.response.setRawBody(jsonResponse);
await kong.response.setHeader('content-type', 'application/json');
} catch (err) {
await kong.response.exit(502, { message: 'SOAP unwrapping failed', detail: err.message });
}
}
}
module.exports = {
Plugin: SoapUnwrapperPlugin,
Schema: [],
Version: '1.0.0',
Priority: 849,
};
Note the priority difference between wrapper (850) and unwrapper (849) — Kong executes response-phase plugins in reverse priority order, so the unwrapper runs before any other response manipulation.
Configuring the plugins in Kong
Once deployed, you wire them up via Kong’s Admin API or declarative YAML:
plugins:
- name: soap-wrapper
service: legacy-crm-service
config:
service_namespace: "http://crm.example.com/CustomerService"
operation: "GetCustomerByIdRequest"
- name: soap-unwrapper
service: legacy-crm-service
This pattern lets you expose a clean REST/JSON API to consumers while still talking SOAP to legacy backends — a common reality in enterprise migrations.
What I miss from DataWeave (and what I don’t)
What I miss: namespace handling is verbose in JS. In DataWeave, ns soap http://schemas.xmlsoap.org/soap/envelope/ is one line. In JS, you're either string-templating the namespace into every element or configuring your XML builder carefully. For complex schemas with multiple namespaces, this gets tedious fast.
What I don’t miss: DataWeave transformations lived inside MuleSoft, versioned separately from your APIs, often in a different team’s hands. Kong plugins live in your repo, versioned with your services, testable with standard JS tooling. npm test beats logging into Anypoint Studio every time.
Lessons from production
A few things we learned after running these plugins in a real enterprise environment:
Test with real SOAP WSDLs, not toy examples. Production SOAP services have deep namespaces, multi-part envelopes, and SOAP faults that your plugin needs to handle gracefully. Build your fault detection early.
Log the raw body before and after transformation during rollout. Kong’s logging plugins make this easy. You’ll catch encoding issues (UTF-8 vs ISO-8859–1 in Spanish language services, for example) before they become incidents.
Set explicit content-length headers. Forgetting to update content-length after rewriting the body causes silent truncation in some upstream services. It's an easy bug and a painful one to diagnose.
Consider a dedicated transformation sidecar for heavy logic. For very complex multi-namespace SOAP services, a lightweight Express microservice sitting between Kong and the legacy backend can be cleaner than a single monolithic plugin. Kong routes to it; it transforms and proxies onward. More components, but easier to test and maintain.
The bottom line
Kong won’t give you DataWeave. Nothing will. But for the two scenarios that matter most in enterprise API migration — XML/JSON conversion and SOAP bridging — JavaScript plugins are a workable, maintainable replacement. The code is longer, the namespace handling is more explicit, and you own more of the error handling. In exchange, you get standard tooling, repo-native versioning, and no platform lock-in.
That’s a trade most teams can live with.
Are you handling more complex scenarios — multi-part SOAP, WS-Security headers, MTOM attachments? I’d love to compare notes in the comments.
메타데이터
- post_id
- b228a83bcb7e
- slug
- replicating-dataweave-transformations-in-kong-gateway-plugins-b228a83bcb7e
- url
- https://medium.com/@lccjhso/replicating-dataweave-transformations-in-kong-gateway-plugins-b228a83bcb7e
- canonical_url
- https://medium.com/@lccjhso/replicating-dataweave-transformations-in-kong-gateway-plugins-b228a83bcb7e
- author_url
- https://medium.com/@lccjhso
- status
- ok
- fetched_at
- 2026-06-11 22:20:54