Skip to main content
Version: Next

body-transformer

Description#

The body-transformer Plugin performs template-based transformations to transform the request and/or response bodies from one format to another, for example, from JSON to JSON, JSON to HTML, or XML to YAML.

Attributes#

NameTypeRequiredDefaultValid valuesDescription
max_req_body_sizeintegerFalse67108864>= 1Maximum request body size in bytes buffered into memory. Requests with a larger body are rejected.
max_resp_body_sizeintegerFalse67108864>= 1Maximum response body size in bytes buffered into memory. Larger responses are truncated.
requestobjectFalseRequest body transformation configuration.
request.input_formatstringFalse[xml,json,encoded,args,plain,multipart]Request body original media type. If unspecified, the value would be determined by the Content-Type header to apply the corresponding decoder. The xml option corresponds to text/xml media type. The json option corresponds to application/json media type. The encoded option corresponds to application/x-www-form-urlencoded media type. The args option corresponds to GET requests. The plain option corresponds to text/plain media type. The multipart option corresponds to multipart/related media type. If the media type is neither type, the value would be left unset and the transformation template will be directly applied.
request.templatestringTrueRequest body transformation template. The template uses lua-resty-template syntax. See the template syntax for more details. 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.
request.template_is_base64booleanFalsefalseSet to true if the template is base64 encoded.
responseobjectFalseResponse body transformation configuration.
response.input_formatstringFalse[xml,json,encoded,args,plain,multipart]Response body original media type. If unspecified, the decoder is determined by the Content-Type header where applicable: xml for text/xml, json for application/json, encoded for application/x-www-form-urlencoded, and multipart for any multipart/* content type. The args option reads URI query args from the request rather than decoding the response body. The plain option must be set explicitly and is not auto-detected from Content-Type: text/plain. If the media type is none of the supported types above, the value will be left unset and the transformation template will be directly applied.
response.templatestringTrueResponse body transformation template.
response.template_is_base64booleanFalsefalseSet to true if the template is base64 encoded.

Examples#

The examples below demonstrate how you can configure body-transformer for different scenarios.

note

You can fetch the admin_key from config.yaml and save to an environment variable with the following command:

admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')

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 you embed a template directly in an Admin API JSON document, encode it as a valid JSON string. For complex templates, base64-encode the content and set template_is_base64 to true.

Transform Between JSON and SOAP XML#

The following example accepts JSON from a client, sends SOAP XML upstream, and returns the SOAP response as JSON. It assumes APISIX is running in the apisix-quickstart-net Docker network created by the quickstart script.

Clone the Spring sample service and check out the revision tested in this example:

git clone "https://github.com/spring-guides/gs-producing-web-service.git"
cd gs-producing-web-service/complete
git switch --detach c9c88ea61cfbcef0de6275c7b6c8b1320549017a

Run the sample in the same network as APISIX:

docker volume create soap-maven-cache
docker rm -f quickstart-soap-service 2>/dev/null || true

docker run -d \
--rm \
--name quickstart-soap-service \
--network apisix-quickstart-net \
-v "$PWD:/workspace" \
-v soap-maven-cache:/root/.m2 \
-w /workspace \
eclipse-temurin:17-jdk@sha256:420933bf319e6aff4bec22eff8d2a31c824df5d853432e3dc0f46d6ea8dd5919 \
./mvnw spring-boot:run

Run docker logs -f quickstart-soap-service and wait for Started ProducingWebServiceApplication. Press Ctrl+C after startup; the service continues running in the background.

Create the SOAP request template:

request-template.xml
cat > request-template.xml <<'EOF'
<?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

Create the JSON response template:

response-template.json
cat > response-template.json <<'EOF'
{% 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

Encode the templates for insertion into the Admin API JSON document:

export REQUEST_TEMPLATE_BASE64="$(base64 < request-template.xml | tr -d '\r\n')"
export RESPONSE_TEMPLATE_BASE64="$(base64 < response-template.json | tr -d '\r\n')"

Create a route that decodes the client request as JSON, sends SOAP XML to the sample service's /services endpoint, decodes the XML response, and returns JSON:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"id": "body-transformer-route",
"methods": ["POST"],
"uri": "/ws",
"plugins": {
"body-transformer": {
"request": {
"template": "$REQUEST_TEMPLATE_BASE64",
"template_is_base64": true,
"input_format": "json"
},
"response": {
"template": "$RESPONSE_TEMPLATE_BASE64",
"template_is_base64": true,
"input_format": "xml"
}
},
"proxy-rewrite": {
"uri": "/services",
"headers": {
"set": {
"Content-Type": "text/xml"
}
}
},
"response-rewrite": {
"headers": {
"set": {
"Content-Type": "application/json"
}
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"quickstart-soap-service:8080": 1
}
}
}
EOF

Send a JSON request to the route:

curl "http://127.0.0.1:9080/ws" -X POST \
-H "Content-Type: application/json" \
-d '{"name":"Spain"}'

You should receive 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.

Create a Route with body-transformer, in which the template appends the word world to the name and adds 10 to the age to set them as values to foo and bar respectively:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_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
}
}
}'

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.

Create a Route with body-transformer, in which the template accesses the request argument using the Nginx variable arg_name:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_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
}
}
}'

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
)

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_key}" \
-d '{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"template": "'"$req_template"'"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

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.

Create a Route with body-transformer which sets the input_format to encoded and configures a template that appends string world to the name input, add 10 to the age input:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_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
}
}
}'

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.

Create a Route with body-transformer, which sets the input_format to args and configures a template that adds a message to the request:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_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
}
}
}'

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.

Create a Route with body-transformer, which sets the input_format to plain and configures a template to remove not and a subsequent space from the body string:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_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
}
}
}'

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'
{%
local core = require 'apisix.core'
local cjson = require 'cjson'

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
)

Create a Route with body-transformer, which sets the input_format to multipart and uses the previously created request template for transformation:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "body-transformer-route",
"uri": "/anything",
"plugins": {
"body-transformer": {
"request": {
"input_format": "multipart",
"template": "'"$req_template"'"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

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
)

Create three Consumers with key-auth configured:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${admin_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_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_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_key}" \
-d '{
"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"'"
}
}
}
}'

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 username field has been renamed to display_name
  • The sensitive phone and balance fields 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 username and phone fields have been filtered out
  • The balance field 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
)

Create Consumers with key-auth configured:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${admin_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_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_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_key}" \
-d '{
"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"'"
}
}
}
}'

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.email has been extracted to top-level email
  • profile.address.city and profile.address.country have been combined into a new location object
  • metadata.created_at has been extracted to top-level created_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 profile object structure has been preserved
  • profile.address.country and metadata fields 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"]
}
}