body-transformer
The body-transformer plugin uses templates to convert request and response bodies between data formats. It can transform either direction independently or apply both transformations to the same request flow.
Examples
Only the examples using mocking generate a response without contacting an upstream. Their ADC and Gateway API configurations include a conventional backend, but mocking answers before it is contacted. The other transformation examples depend on the actual upstream response.
The examples below demonstrate how you can configure body-transformer for different scenarios.
The transformation template uses lua-resty-template syntax. See the template syntax to learn more.
You can also use auxiliary functions _escape_json() and _escape_xml() to escape special characters such as double quotes, _body to access request body, and _ctx to access context variables.
When using JSON-based configuration, encode the transformation template as a valid JSON string.
Transform between JSON and XML SOAP
The following example demonstrates how to transform a JSON request into a SOAP envelope and convert the SOAP response back to JSON. It uses WireMock to provide a deterministic SOAP response without depending on a moving application repository.
Start the SOAP Upstream
Set up the WireMock fixture with Docker for the Admin API and ADC examples, or deploy it to Kubernetes for the Ingress Controller example.
- Docker
- Kubernetes
Create directories for the WireMock mapping and response body:
mkdir -p body-transformer-wiremock/mappings body-transformer-wiremock/__files
Create the request mapping:
cat > body-transformer-wiremock/mappings/country.json <<'EOF'
{
"request": {
"method": "POST",
"urlPath": "/services",
"bodyPatterns": [
{
"matchesXPath": "//*[local-name()='getCountryRequest']/*[local-name()='name' and text()='Spain']"
}
]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "text/xml; charset=utf-8"
},
"bodyFileName": "country-response.xml"
}
}
EOF
Create the SOAP response body returned by the mapping:
cat > body-transformer-wiremock/__files/country-response.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<ns2:getCountryResponse xmlns:ns2="https://spring.io/guides/gs-producing-web-service">
<ns2:country>
<ns2:name>Spain</ns2:name>
<ns2:population>46704314</ns2:population>
<ns2:capital>Madrid</ns2:capital>
<ns2:currency>EUR</ns2:currency>
</ns2:country>
</ns2:getCountryResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
EOF
Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container, then place the gateway and WireMock on a dedicated network:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-body-transformer-net
docker network connect gateway-body-transformer-net "$GATEWAY_CONTAINER"
docker run -d \
--name body-transformer-soap \
--network gateway-body-transformer-net \
-v "$PWD/body-transformer-wiremock:/home/wiremock:ro" \
wiremock/wiremock:3.13.2
Create the WireMock fixture, Deployment, and Service:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: aic
name: body-transformer-soap
data:
country.json: |
{
"request": {
"method": "POST",
"urlPath": "/services",
"bodyPatterns": [
{
"matchesXPath": "//*[local-name()='getCountryRequest']/*[local-name()='name' and text()='Spain']"
}
]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "text/xml; charset=utf-8"
},
"bodyFileName": "country-response.xml"
}
}
country-response.xml: |
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<ns2:getCountryResponse xmlns:ns2="https://spring.io/guides/gs-producing-web-service">
<ns2:country>
<ns2:name>Spain</ns2:name>
<ns2:population>46704314</ns2:population>
<ns2:capital>Madrid</ns2:capital>
<ns2:currency>EUR</ns2:currency>
</ns2:country>
</ns2:getCountryResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: body-transformer-soap
spec:
replicas: 1
selector:
matchLabels:
app: body-transformer-soap
template:
metadata:
labels:
app: body-transformer-soap
spec:
containers:
- name: wiremock
image: wiremock/wiremock:3.13.2
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: fixture
mountPath: /home/wiremock/mappings/country.json
subPath: country.json
readOnly: true
- name: fixture
mountPath: /home/wiremock/__files/country-response.xml
subPath: country-response.xml
readOnly: true
volumes:
- name: fixture
configMap:
name: body-transformer-soap
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: body-transformer-soap
spec:
selector:
app: body-transformer-soap
ports:
- name: http
port: 8080
targetPort: http
Apply the fixture to the cluster:
kubectl apply -f soap-upstream.yaml
Configure the Route
Create the request and response transformation templates:
req_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n'
<?xml version="1.0"?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Body>
<ns0:getCountryRequest xmlns:ns0="https://spring.io/guides/gs-producing-web-service">
<ns0:name>{{_escape_xml(name)}}</ns0:name>
</ns0:getCountryRequest>
</soap-env:Body>
</soap-env:Envelope>
EOF
)
rsp_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n'
{% if Envelope.Body.Fault == nil then %}
{
"status":"{{_ctx.var.status}}",
"currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
"population":{{Envelope.Body.getCountryResponse.country.population}},
"capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
"name":"{{Envelope.Body.getCountryResponse.country.name}}"
}
{% else %}
{
"message":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},
"code":"{{Envelope.Body.Fault.faultcode}}"
{% if Envelope.Body.Fault.faultactor ~= nil then %}
, "actor":"{{Envelope.Body.Fault.faultactor}}"
{% end %}
}
{% end %}
EOF
)
awk and tr are used above to manipulate the template such that the template would be a valid JSON string.
Create a route with body-transformer using the templates created previously:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/body-transformer-soap" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d @- <<EOF
{
"methods": ["POST"],
"uri": "/services",
"plugins": {
"body-transformer": {
"request": {
"template": "$req_template",
"input_format": "json"
},
"response": {
"template": "$rsp_template",
"input_format": "xml"
}
},
"proxy-rewrite": {
"headers": {
"set": {
"Content-Type": "text/xml"
}
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"body-transformer-soap:8080": 1
}
}
}
EOF
Create a route with body-transformer using the templates created previously:
services:
- name: body-transformer-service
labels:
docs-example: body-transformer-soap
routes:
- name: body-transformer-route
methods:
- POST
uris:
- /services
plugins:
body-transformer:
request:
template: |
<?xml version="1.0"?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Body>
<ns0:getCountryRequest xmlns:ns0="https://spring.io/guides/gs-producing-web-service">
<ns0:name>{{_escape_xml(name)}}</ns0:name>
</ns0:getCountryRequest>
</soap-env:Body>
</soap-env:Envelope>
input_format: json
response:
template: |
{% if Envelope.Body.Fault == nil then %}
{
"status":"{{_ctx.var.status}}",
"currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
"population":{{Envelope.Body.getCountryResponse.country.population}},
"capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
"name":"{{Envelope.Body.getCountryResponse.country.name}}"
}
{% else %}
{
"message":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},
"code":"{{Envelope.Body.Fault.faultcode}}"
{% if Envelope.Body.Fault.faultactor ~= nil then %}
, "actor":"{{Envelope.Body.Fault.faultactor}}"
{% end %}
}
{% end %}
input_format: xml
proxy-rewrite:
headers:
set:
Content-Type: text/xml
upstream:
type: roundrobin
nodes:
- host: body-transformer-soap
port: 8080
weight: 1
Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=body-transformer-soap
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=body-transformer-soap
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: json-xml-plugin
spec:
plugins:
- name: body-transformer
config:
request:
template: |
<?xml version="1.0"?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Body>
<ns0:getCountryRequest xmlns:ns0="https://spring.io/guides/gs-producing-web-service">
<ns0:name>{{_escape_xml(name)}}</ns0:name>
</ns0:getCountryRequest>
</soap-env:Body>
</soap-env:Envelope>
input_format: json
response:
template: |
{% if Envelope.Body.Fault == nil then %}
{
"status":"{{_ctx.var.status}}",
"currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
"population":{{Envelope.Body.getCountryResponse.country.population}},
"capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
"name":"{{Envelope.Body.getCountryResponse.country.name}}"
}
{% else %}
{
"message":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},
"code":"{{Envelope.Body.Fault.faultcode}}"
{% if Envelope.Body.Fault.faultactor ~= nil then %}
, "actor":"{{Envelope.Body.Fault.faultactor}}"
{% end %}
}
{% end %}
input_format: xml
- name: proxy-rewrite
config:
headers:
set:
Content-Type: text/xml
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- method: POST
path:
type: Exact
value: /services
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: json-xml-plugin
backendRefs:
- name: body-transformer-soap
port: 8080
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /services
methods:
- POST
plugins:
- name: body-transformer
enable: true
config:
request:
template: |
<?xml version="1.0"?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Body>
<ns0:getCountryRequest xmlns:ns0="https://spring.io/guides/gs-producing-web-service">
<ns0:name>{{_escape_xml(name)}}</ns0:name>
</ns0:getCountryRequest>
</soap-env:Body>
</soap-env:Envelope>
input_format: json
response:
template: |
{% if Envelope.Body.Fault == nil then %}
{
"status":"{{_ctx.var.status}}",
"currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
"population":{{Envelope.Body.getCountryResponse.country.population}},
"capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
"name":"{{Envelope.Body.getCountryResponse.country.name}}"
}
{% else %}
{
"message":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},
"code":"{{Envelope.Body.Fault.faultcode}}"
{% if Envelope.Body.Fault.faultactor ~= nil then %}
, "actor":"{{Envelope.Body.Fault.faultactor}}"
{% end %}
}
{% end %}
input_format: xml
- name: proxy-rewrite
enable: true
config:
headers:
set:
Content-Type: text/xml
upstreams:
- name: body-transformer-soap
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: body-transformer-soap
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: body-transformer-soap.aic.svc
port: 8080
Apply the configuration to your cluster:
kubectl apply -f soap-route.yaml
❶ Set the request input format as JSON, so that the plugin will apply the JSON decoder internally.
❷ Set the response input format as XML, so that the plugin will apply the XML decoder internally.
❸ Set the Content-Type header to text/xml for the upstream SOAP service to respond properly.
❹ Use the WireMock container address for the Admin API and ADC examples. The Ingress Controller example uses the Kubernetes Service address.
For large templates, encode each file as a portable single-line base64 value:
request_template=$(base64 < request-template.txt | tr -d '\n')
response_template=$(base64 < response-template.txt | tr -d '\n')
Set each template field to the corresponding encoded value and set template_is_base64 to true.
Verify
Send a request with a valid JSON body:
curl "http://127.0.0.1:9080/services" -X POST -d '{"name": "Spain"}'
The plugin transforms the JSON request into XML before forwarding it to the SOAP upstream. It transforms the XML response back into JSON.
You should see a response similar to the following:
{
"status": "200",
"currency": "EUR",
"population": 46704314,
"capital": "Madrid",
"name": "Spain"
}
Modify Request Body
The following example demonstrates how to dynamically modify the request body.
- Admin API
- ADC
- Ingress Controller
Create a route with body-transformer:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"template": "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a route with body-transformer:
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /anything
plugins:
body-transformer:
request:
template: "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: body-transformer
config:
request:
template: "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: body-transformer
enable: true
config:
request:
template: "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Set a template that appends "world" to the name and adds 10 to the age and set them as values to "foo" and "bar" respectively.
Send a request to the route:
curl "http://127.0.0.1:9080/anything" -X POST \
-H "Content-Type: application/json" \
-d '{"name":"hello","age":20}' \
-i
You should see a response of the following:
{
"args": {},
"data": "{\"foo\":\"hello world\",\"bar\":30}",
...
"json": {
"bar": 30,
"foo": "hello world"
},
"method": "POST",
...
}
Generate Request Body Using Variables
The following example demonstrates how to generate request body dynamically using the ctx context variables.
- Admin API
- ADC
- Ingress Controller
Create a route with body-transformer:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"template": "{\"foo\":\"{{_ctx.var.arg_name .. \" world\"}}\"}"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a route with body-transformer:
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /anything
plugins:
body-transformer:
request:
template: "{\"foo\":\"{{_ctx.var.arg_name .. \" world\"}}\"}"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: body-transformer
config:
request:
template: "{\"foo\":\"{{_ctx.var.arg_name .. \" world\"}}\"}"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: body-transformer
enable: true
config:
request:
template: "{\"foo\":\"{{_ctx.var.arg_name .. \" world\"}}\"}"
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Set a template which accesses the request argument using the NGINX variable arg_name.
Send a request to the route with name argument:
curl -i "http://127.0.0.1:9080/anything?name=hello"
You should see a response like this:
{
"args": {
"name": "hello"
},
...,
"json": {
"foo": "hello world"
},
...
}
Transform Body from YAML to JSON
The following example demonstrates how to transform request body from YAML to JSON.
Create the request transformation template:
req_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1'
{%
local yaml = require("tinyyaml")
local body = yaml.parse(_body)
%}
{"foobar":"{{body.foobar.foo .. " " .. body.foobar.bar}}"}
EOF
)
- Admin API
- ADC
- Ingress Controller
Create a route with body-transformer that uses the template:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @- <<EOF
{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"template": "$req_template"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
Create a route with body-transformer that uses the template:
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /anything
plugins:
body-transformer:
request:
template: |
{%
local yaml = require("tinyyaml")
local body = yaml.parse(_body)
%}
{"foobar":"{{body.foobar.foo .. " " .. body.foobar.bar}}"}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: body-transformer
config:
request:
template: |
{%
local yaml = require("tinyyaml")
local body = yaml.parse(_body)
%}
{"foobar":"{{body.foobar.foo .. " " .. body.foobar.bar}}"}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: body-transformer
enable: true
config:
request:
template: |
{%
local yaml = require("tinyyaml")
local body = yaml.parse(_body)
%}
{"foobar":"{{body.foobar.foo .. " " .. body.foobar.bar}}"}
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
Send a request to the route with a YAML body:
body='
foobar:
foo: hello
bar: world'
curl "http://127.0.0.1:9080/anything" -X POST \
-d "$body" \
-H "Content-Type: text/yaml" \
-i
You should see a response similar to the following, which verifies that the YAML body was appropriately transformed to JSON:
{
"args": {},
"data": "{\"foobar\":\"hello world\"}",
...
"json": {
"foobar": "hello world"
},
...
}
Transform Form URL Encoded Body to JSON
The following example demonstrates how to transform form-urlencoded body to JSON.
- Admin API
- ADC
- Ingress Controller
Create a route with body-transformer as such:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"input_format": "encoded",
"template": "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a route with body-transformer as such:
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /anything
plugins:
body-transformer:
request:
input_format: encoded
template: "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: body-transformer
config:
request:
input_format: encoded
template: "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: body-transformer
enable: true
config:
request:
input_format: encoded
template: "{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}"
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Set the input_format to encoded.
❷ Set a template which appends string world to the name input, add 10 to the age input, and form a new JSON object.
Send a POST request to the route with an encoded body:
curl "http://127.0.0.1:9080/anything" -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'name=hello&age=20'
You should see a response similar to the following:
{
"args": {},
"data": "",
"files": {},
"form": {
"{\"foo\":\"hello world\",\"bar\":30}": ""
},
"headers": {
...
},
...
}
Transform GET Request Query Parameter to Body
The following example demonstrates how to transform a GET request query parameter to request body. Note that this does not transform the HTTP method. To transform the method, see proxy-rewrite.
- Admin API
- ADC
- Ingress Controller
Create a route with body-transformer as such:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"input_format": "args",
"template": "{\"message\": \"hello {{name}}\"}"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a route with body-transformer as such:
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /anything
plugins:
body-transformer:
request:
input_format: args
template: '{"message": "hello {{name}}"}'
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: body-transformer
config:
request:
input_format: args
template: '{"message": "hello {{name}}"}'
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: body-transformer
enable: true
config:
request:
input_format: args
template: '{"message": "hello {{name}}"}'
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Set the input_format to args.
❷ Set a template which adds a message to the request.
Send a GET request to the route:
curl "http://127.0.0.1:9080/anything?name=john"
You should see a response similar to the following:
{
"args": {},
"data": "{\"message\": \"hello john\"}",
"files": {},
"form": {},
"headers": {
...
},
"json": {
"message": "hello john"
},
"method": "GET",
...
}
Transform Plain Media Type
The following example demonstrates how to transform requests with plain media type.
- Admin API
- ADC
- Ingress Controller
Create a route with body-transformer as such:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"input_format": "plain",
"template": "{\"message\": \"{* string.gsub(_body, \"not \", \"\") *}\"}"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a route with body-transformer as such:
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /anything
plugins:
body-transformer:
request:
input_format: plain
template: "{\"message\": \"{* string.gsub(_body, \"not \", \"\") *}\"}"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: body-transformer
config:
request:
input_format: plain
template: "{\"message\": \"{* string.gsub(_body, \"not \", \"\") *}\"}"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: body-transformer
enable: true
config:
request:
input_format: plain
template: "{\"message\": \"{* string.gsub(_body, \"not \", \"\") *}\"}"
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Set the input_format to plain.
❷ Set a template which removes not and a subsequent space from the body string.
Send a POST request to the route:
curl "http://127.0.0.1:9080/anything" -X POST \
-d 'not actually json' \
-i
You should see a response similar to the following:
{
"args": {},
"data": "",
"files": {},
"form": {
"{\"message\": \"actually json\"}": ""
},
"headers": {
...
},
...
}
Transform Multipart Media Type
The following example demonstrates how to transform requests with multipart media type.
Create a request transformation template which adds a status to the body based on the age provided in the request body:
req_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1'
{%
if tonumber(context.age) > 18 then
context._multipart:set_simple("status", "adult")
else
context._multipart:set_simple("status", "minor")
end
local body = context._multipart:tostring()
%}{* body *}
EOF
)
- Admin API
- ADC
- Ingress Controller
Create a route with body-transformer as such:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @- <<EOF
{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"input_format": "multipart",
"template": "$req_template"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
Create a route with body-transformer as such:
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /anything
plugins:
body-transformer:
request:
input_format: multipart
template: |
{%
if tonumber(context.age) > 18 then
context._multipart:set_simple("status", "adult")
else
context._multipart:set_simple("status", "minor")
end
local body = context._multipart:tostring()
%}{* body *}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: body-transformer
config:
request:
input_format: multipart
template: |
{%
if tonumber(context.age) > 18 then
context._multipart:set_simple("status", "adult")
else
context._multipart:set_simple("status", "minor")
end
local body = context._multipart:tostring()
%}{* body *}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create a Kubernetes manifest file of a route with the body-transformer plugin:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: body-transformer
enable: true
config:
request:
input_format: multipart
template: |
{%
if tonumber(context.age) > 18 then
context._multipart:set_simple("status", "adult")
else
context._multipart:set_simple("status", "minor")
end
local body = context._multipart:tostring()
%}{* body *}
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Set the input_format to multipart.
❷ Set to the previously created request template.
Send a multipart POST request to the route:
curl -X POST \
-F "name=john" \
-F "age=10" \
"http://127.0.0.1:9080/anything"
You should see a response similar to the following:
{
"args": {},
"data": "",
"files": {},
"form": {
"age": "10",
"name": "john",
"status": "minor"
},
"headers": {
"Accept": "*/*",
"Content-Length": "361",
"Content-Type": "multipart/form-data; boundary=------------------------qtPjk4c8ZjmGOXNKzhqnOP",
...
},
...
}
Transform Response Body Based on Consumer Identity
The following example demonstrates how to customize response body transformations based on different consumer identities. The example shows how to return different response formats to different consumers while filtering sensitive fields and renaming properties.
Create the response transformation template that applies different transformations based on the consumer identity:
rsp_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n'
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "consumer-a" then %}
{
"user_id": {* user_id *},
"display_name": {* _escape_json(username) *},
"email": {* _escape_json(email) *}
}
{% elseif consumer_name == "consumer-b" then %}
{
"user_id": {* user_id *},
"email": {* _escape_json(email) *},
"balance": {* balance *}
}
{% else %}
{* _body *}
{% end %}
EOF
)
- Admin API
- ADC
- Ingress Controller
Create three consumers with key-auth configured:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "consumer-a",
"plugins": {
"key-auth": {
"key": "consumer-a"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "consumer-b",
"plugins": {
"key-auth": {
"key": "consumer-b"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "consumer-c",
"plugins": {
"key-auth": {
"key": "consumer-c"
}
}
}'
Create a route with body-transformer, key-auth, and mocking plugins:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @- <<EOF
{
"id": "body-transformer-route",
"uri": "/mock",
"plugins": {
"key-auth": {},
"mocking": {
"response_example": "{\"user_id\":1001,\"username\":\"john_doe\",\"email\":\"john@example.com\",\"phone\":\"+1-555-0123\",\"balance\":1250.50}"
},
"body-transformer": {
"response": {
"input_format": "json",
"template": "$rsp_template"
}
}
}
}
EOF
Create three consumers with key-auth configured and a route with body-transformer, key-auth, and mocking plugins:
consumers:
- username: consumer-a
credentials:
- name: key-auth
type: key-auth
config:
key: consumer-a
- username: consumer-b
credentials:
- name: key-auth
type: key-auth
config:
key: consumer-b
- username: consumer-c
credentials:
- name: key-auth
type: key-auth
config:
key: consumer-c
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /mock
plugins:
key-auth: {}
mocking:
response_example: '{"user_id":1001,"username":"john_doe","email":"john@example.com","phone":"+1-555-0123","balance":1250.50}'
body-transformer:
response:
input_format: json
template: |
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "consumer-a" then %}
{
"user_id": {* user_id *},
"display_name": {* _escape_json(username) *},
"email": {* _escape_json(email) *}
}
{% elseif consumer_name == "consumer-b" then %}
{
"user_id": {* user_id *},
"email": {* _escape_json(email) *},
"balance": {* balance *}
}
{% else %}
{* _body *}
{% end %}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
Create three consumers with key-auth configured and a route with body-transformer, key-auth, and mocking plugins:
When consumers are configured using the Ingress Controller, the consumer name is generated in the format namespace_consumername. As a result, the consumer_name logic in the template should match the consumer name in this format.
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: consumer-a
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: consumer-a
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: consumer-b
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: consumer-b
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: consumer-c
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: consumer-c
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: key-auth
config:
_meta:
disable: false
- name: mocking
config:
response_example: '{"user_id":1001,"username":"john_doe","email":"john@example.com","phone":"+1-555-0123","balance":1250.50}'
- name: body-transformer
config:
response:
input_format: json
template: |
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "aic_consumer-a" then %}
{
"user_id": {* user_id *},
"display_name": {* _escape_json(username) *},
"email": {* _escape_json(email) *}
}
{% elseif consumer_name == "aic_consumer-b" then %}
{
"user_id": {* user_id *},
"email": {* _escape_json(email) *},
"balance": {* balance *}
}
{% else %}
{* _body *}
{% end %}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /mock
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: consumer-a
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: consumer-a
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: consumer-b
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: consumer-b
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: consumer-c
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: consumer-c
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /mock
upstreams:
- name: httpbin-external-domain
plugins:
- name: key-auth
enable: true
- name: mocking
enable: true
config:
response_example: '{"user_id":1001,"username":"john_doe","email":"john@example.com","phone":"+1-555-0123","balance":1250.50}'
- name: body-transformer
enable: true
config:
response:
input_format: json
template: |
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "aic_consumer-a" then %}
{
"user_id": {* user_id *},
"display_name": {* _escape_json(username) *},
"email": {* _escape_json(email) *}
}
{% elseif consumer_name == "aic_consumer-b" then %}
{
"user_id": {* user_id *},
"email": {* _escape_json(email) *},
"balance": {* balance *}
}
{% else %}
{* _body *}
{% end %}
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Configure the mocking plugin to return a sample upstream response.
❷ Set the response input format as JSON.
❸ Set the transformation template that customizes the response based on consumer identity.
Send requests with different apikey headers to verify the response transformations:
Send a request as consumer-a:
curl "http://127.0.0.1:9080/mock" -H "apikey: consumer-a"
You should see a response similar to the following, which demonstrates these transformations:
- The
usernamefield has been renamed todisplay_name - The sensitive
phoneandbalancefields have been filtered out
{
"user_id": 1001,
"display_name": "john_doe",
"email": "john@example.com"
}
Send a request as consumer-b:
curl "http://127.0.0.1:9080/mock" -H "apikey: consumer-b"
You should see a response similar to the following, which demonstrates these transformations:
- The
usernameandphonefields have been filtered out - The
balancefield has been preserved
{
"user_id": 1001,
"email": "john@example.com",
"balance": 1250.50
}
Send a request as consumer-c:
curl "http://127.0.0.1:9080/mock" -H "apikey: consumer-c"
You should see a response similar to the following, which shows that the original response is returned unchanged:
{
"user_id": 1001,
"username": "john_doe",
"email": "john@example.com",
"phone": "+1-555-0123",
"balance": 1250.50
}
Transform Nested Response Body Based on Consumer Identity
The following example demonstrates how to customize response body transformations based on different consumer identities. The example shows how to extract nested fields, reorganize data structures, and flatten nested objects while providing different response formats to different consumers based on their identity.
Create the response transformation template that extracts and reorganizes nested JSON fields based on the consumer identity:
rsp_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n'
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "consumer-a" then %}
{
"user_id": {* id *},
"user_name": {* _escape_json(name) *},
"email": {* _escape_json(profile.email) *},
"location": {
"city": {* _escape_json(profile.address.city) *},
"country": {* _escape_json(profile.address.country) *}
},
"created_at": {* _escape_json(metadata.created_at) *}
}
{% elseif consumer_name == "consumer-b" then %}
{
"id": {* id *},
"name": {* _escape_json(name) *},
"status": {* _escape_json(status) *},
"profile": {
"email": {* _escape_json(profile.email) *},
"address": {
"city": {* _escape_json(profile.address.city) *}
}
}
}
{% else %}
{* _body *}
{% end %}
EOF
)
- Admin API
- ADC
- Ingress Controller
Create consumers with key-auth configured:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "consumer-a",
"plugins": {
"key-auth": {
"key": "consumer-a"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "consumer-b",
"plugins": {
"key-auth": {
"key": "consumer-b"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "consumer-c",
"plugins": {
"key-auth": {
"key": "consumer-c"
}
}
}'
Create a route with body-transformer, key-auth, and mocking plugins using the nested structure template:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @- <<EOF
{
"id": "body-transformer-route",
"uri": "/mock",
"plugins": {
"key-auth": {},
"mocking": {
"response_example": "{\"id\":123,\"name\":\"John Doe\",\"status\":\"active\",\"profile\":{\"email\":\"john@example.com\",\"address\":{\"city\":\"New York\",\"country\":\"USA\"}},\"metadata\":{\"created_at\":\"2024-01-01\",\"tags\":[\"vip\",\"premium\"]}}"
},
"body-transformer": {
"response": {
"input_format": "json",
"template": "$rsp_template"
}
}
}
}
EOF
Create consumers with key-auth configured and a route with body-transformer, key-auth, and mocking plugins:
consumers:
- username: consumer-a
credentials:
- name: key-auth
type: key-auth
config:
key: consumer-a
- username: consumer-b
credentials:
- name: key-auth
type: key-auth
config:
key: consumer-b
- username: consumer-c
credentials:
- name: key-auth
type: key-auth
config:
key: consumer-c
services:
- name: body-transformer-service
routes:
- name: body-transformer-route
uris:
- /mock
plugins:
key-auth: {}
mocking:
response_example: '{"id":123,"name":"John Doe","status":"active","profile":{"email":"john@example.com","address":{"city":"New York","country":"USA"}},"metadata":{"created_at":"2024-01-01","tags":["vip","premium"]}}'
body-transformer:
response:
input_format: json
template: |
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "consumer-a" then %}
{
"user_id": {* id *},
"user_name": {* _escape_json(name) *},
"email": {* _escape_json(profile.email) *},
"location": {
"city": {* _escape_json(profile.address.city) *},
"country": {* _escape_json(profile.address.country) *}
},
"created_at": {* _escape_json(metadata.created_at) *}
}
{% elseif consumer_name == "consumer-b" then %}
{
"id": {* id *},
"name": {* _escape_json(name) *},
"status": {* _escape_json(status) *},
"profile": {
"email": {* _escape_json(profile.email) *},
"address": {
"city": {* _escape_json(profile.address.city) *}
}
}
}
{% else %}
{* _body *}
{% end %}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
Create consumers with key-auth configured and a route with body-transformer, key-auth, and mocking plugins:
When consumers are configured using the Ingress Controller, the consumer name is generated in the format namespace_consumername. As a result, the consumer_name logic in the template should match the consumer name in this format.
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: consumer-a
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: consumer-a
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: consumer-b
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: consumer-b
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: consumer-c
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: consumer-c
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: body-transformer-plugin-config
spec:
plugins:
- name: key-auth
config:
_meta:
disable: false
- name: mocking
config:
response_example: '{"id":123,"name":"John Doe","status":"active","profile":{"email":"john@example.com","address":{"city":"New York","country":"USA"}},"metadata":{"created_at":"2024-01-01","tags":["vip","premium"]}}'
- name: body-transformer
config:
response:
input_format: json
template: |
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "aic_consumer-a" then %}
{
"user_id": {* id *},
"user_name": {* _escape_json(name) *},
"email": {* _escape_json(profile.email) *},
"location": {
"city": {* _escape_json(profile.address.city) *},
"country": {* _escape_json(profile.address.country) *}
},
"created_at": {* _escape_json(metadata.created_at) *}
}
{% elseif consumer_name == "aic_consumer-b" then %}
{
"id": {* id *},
"name": {* _escape_json(name) *},
"status": {* _escape_json(status) *},
"profile": {
"email": {* _escape_json(profile.email) *},
"address": {
"city": {* _escape_json(profile.address.city) *}
}
}
}
{% else %}
{* _body *}
{% end %}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /mock
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: body-transformer-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: consumer-a
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: consumer-a
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: consumer-b
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: consumer-b
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: consumer-c
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: consumer-c
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: body-transformer-route
spec:
ingressClassName: apisix
http:
- name: body-transformer-route
match:
paths:
- /mock
upstreams:
- name: httpbin-external-domain
plugins:
- name: key-auth
enable: true
- name: mocking
enable: true
config:
response_example: '{"id":123,"name":"John Doe","status":"active","profile":{"email":"john@example.com","address":{"city":"New York","country":"USA"}},"metadata":{"created_at":"2024-01-01","tags":["vip","premium"]}}'
- name: body-transformer
enable: true
config:
response:
input_format: json
template: |
{% local consumer_name = _ctx.consumer and _ctx.consumer.username or "" %}
{% if consumer_name == "aic_consumer-a" then %}
{
"user_id": {* id *},
"user_name": {* _escape_json(name) *},
"email": {* _escape_json(profile.email) *},
"location": {
"city": {* _escape_json(profile.address.city) *},
"country": {* _escape_json(profile.address.country) *}
},
"created_at": {* _escape_json(metadata.created_at) *}
}
{% elseif consumer_name == "aic_consumer-b" then %}
{
"id": {* id *},
"name": {* _escape_json(name) *},
"status": {* _escape_json(status) *},
"profile": {
"email": {* _escape_json(profile.email) *},
"address": {
"city": {* _escape_json(profile.address.city) *}
}
}
}
{% else %}
{* _body *}
{% end %}
Apply the configuration to your cluster:
kubectl apply -f body-transformer-ic.yaml
❶ Configure the mocking plugin to return a sample nested upstream response.
❷ Set the response input format as JSON.
❸ Set the transformation template that handles nested JSON structures based on consumer identity.
Send a request as consumer-a to verify the nested structure transformation:
curl "http://127.0.0.1:9080/mock" -H "apikey: consumer-a"
You should see a response similar to the following, which demonstrates these nested field transformations:
profile.emailhas been extracted to top-levelemailprofile.address.cityandprofile.address.countryhave been combined into a newlocationobjectmetadata.created_athas been extracted to top-levelcreated_at
{
"user_id": 123,
"user_name": "John Doe",
"email": "john@example.com",
"location": {
"city": "New York",
"country": "USA"
},
"created_at": "2024-01-01"
}
Send a request as consumer-b to verify the nested structure transformation:
curl "http://127.0.0.1:9080/mock" -H "apikey: consumer-b"
You should see a response similar to the following, which demonstrates these transformations:
- The original
profileobject structure has been preserved profile.address.countryandmetadatafields have been filtered out
{
"id": 123,
"name": "John Doe",
"status": "active",
"profile": {
"email": "john@example.com",
"address": {
"city": "New York"
}
}
}
Send a request as consumer-c to verify the nested structure transformation:
curl "http://127.0.0.1:9080/mock" -H "apikey: consumer-c"
You should see a response similar to the following, which shows that the original nested response is returned unchanged:
{
"id": 123,
"name": "John Doe",
"status": "active",
"profile": {
"email": "john@example.com",
"address": {
"city": "New York",
"country": "USA"
}
},
"metadata": {
"created_at": "2024-01-01",
"tags": ["vip", "premium"]
}
}