Skip to main content

Transforming JSON Responses

Overview#

The REST API allows you to retrieve information about a device using the FetchDevice endpoint. The JSON responses for devices can, however, be quite large, especially in the case of Bluetooth LE devices. For example, here is what the JSON for a Thingy:52 device might look like:

{  "id": "CA:B2:31:EE:E0:9E",  "name": "CA:B2:31:EE:E0:9E",  "type": "BLE",  "tags": [],  "state": {    "1800": {      "uuid": "1800",      "characteristics": {        "2A00": {          "uuid": "2A00",          "path": "1800/2A00",          "value": [            65,            112,            112,            108,            101,            32,            84,            86          ],          "properties": {            "broadcast": false,            "read": true,            "writeWithoutResponse": false,            "write": false,            "notify": false,            "indicate": false,            "authorizedSignedWrite": false          },          "descriptors": {}        },        "2A01": {          "uuid": "2A01",          "path": "1800/2A01",          "value": [            128,            2          ],          "properties": {            "broadcast": false,            "read": true,            "writeWithoutResponse": false,            "write": false,            "notify": false,            "indicate": false,            "authorizedSignedWrite": false          },          "descriptors": {}        }      }    },    "1801": {      "uuid": "1801",      "characteristics": {        "2A05": {          "uuid": "2A05",          "path": "1801/2A05",          "value": [],          "properties": {            "broadcast": false,            "read": false,            "writeWithoutResponse": false,            "write": false,            "notify": false,            "indicate": true,            "authorizedSignedWrite": false          },          "descriptors": {            "2902": {              "uuid": "2902",              "value": [                0,                0              ],              "path": "1801/2A05/2902"            }          }        }      }    },    "D0611E78BBB44591A5F8487910AE4366": {      "uuid": "D0611E78BBB44591A5F8487910AE4366",      "characteristics": {        "8667556C9A374C9184ED54EE27D90049": {          "uuid": "8667556C9A374C9184ED54EE27D90049",          "path": "D0611E78BBB44591A5F8487910AE4366/8667556C9A374C9184ED54EE27D90049",          "value": [],          "properties": {            "broadcast": false,            "read": false,            "writeWithoutResponse": false,            "write": true,            "notify": true,            "indicate": false,            "authorizedSignedWrite": false          },          "descriptors": {            "2900": {              "uuid": "2900",              "value": [                1,                0              ],              "path": "D0611E78BBB44591A5F8487910AE4366/8667556C9A374C9184ED54EE27D90049/2900"            },            "2902": {              "uuid": "2902",              "value": [                0,                0              ],              "path": "D0611E78BBB44591A5F8487910AE4366/8667556C9A374C9184ED54EE27D90049/2902"            }          }        }      }    }  },  "$meta": {    "version": "1.0",    "createdAt": "2018-11-06T22:20:15.417Z",    "updatedAt": "2018-11-06T22:20:44.676Z"  }}

While it's great that you can get so much detail, but in many cases you will only be interested in a subset of information. Fortunately, by using a "transform" you can control what is returned to you.

What is a Transform?#

A transform is a JSONata expression that you send as a querystring parameter, and which causes one or more resources in the default JSON response to be transformed accordingly. From the end user's perspective you can think of JSONata as a REST-based analog to GraphQL: you get to express what the response should contain, and how it is shaped. (GraphQL support for the API is under serious consideration for future releases.)

The API provides transform support only for FetchDevice and ListDevices endpoints.

The easiest way to learn about JSONata transforms is through examples. Let's get started!

Using Transforms#

If you wanted to just grab the ids of all your devices, you could do the following:

export API_KEY=YOUR_API_KEYcurl --request GET \  --url 'https://api.nrfcloud.com/v1/devices?includeState=true&transform=id' \  --header 'Authorization: Bearer $API_KEY'

Instead of all the device data, you will only receive something like this:

{    "items": [        "CA:B2:31:EE:E0:9E",        "ba4130fc-5d1f-423b-bd1c-05213932a884",        "MyGenericDevice1"    ],    "total": 3}

What if you want ids only for Bluetooth LE devices?

curl --request GET \  --url 'https://api.nrfcloud.com/v1/devices?includeState=true&transform=type="BLE" ? id' \  --header 'Authorization: Bearer $API_KEY'

Voila! Note that null is found for devices that did not match the transform.

{    "items": [        "CA:B2:31:EE:E0:9E",        null,        null    ],    "total": 3}

However, in the case of filtering device types, it's best to use the deviceTypes parameter and pass in BLE:

curl --request GET \  --url 'https://api.nrfcloud.com/v1/devices?transform=id&deviceTypes=BLE' \  --header 'Authorization: Bearer $API_KEY'

Now let's say you want to get a JSON object containing the id and type of all your devices:

curl --request GET \  --url 'https://api.nrfcloud.com/v1/devices?includeState=true&transform={ "id": id, "type": type }' \  --header 'Authorization: Bearer $API_KEY'

This request should return something like the following:

{    "items": [        {            "id": "CA:B2:31:EE:E0:9E",            "type": "BLE"        },        {            "id": "ba4130fc-5d1f-423b-bd1c-05213932a884",            "type": "Gateway"        },        {            "id": "MyGenericDevice1",            "type": "Generic"        }    ],    "total": 3}

There are a few things to take note of with this example:

  1. You should wrap the URL and its parameters in single quotes, because JSONata requires valid JSON, which requires double quotes for field names.
  2. In the transform, values that are not wrapped in quotes are considered variables that map to the resource being transformed. In this case, the JSON for the resource contains both an id and type property, which we return as values for fields of the same name (but you could give the field any name you'd like, e.g., deviceId and deviceType.)

So far we've only worked with top level properties of the Device resource. You can also use a transform to retrieve and shape data deep within the resource's JSON representation. For example, here's how you would retrieve only the characteristics and their values from the Bluetooth LE peripheral whose rather large JSON representation was shown above:

curl --request GET \  --url 'https://api.nrfcloud.com/v1/devices/CA:B2:31:EE:E0:9E?transform={ "id": id, "characteristics": $map(state.*.characteristics.*, function($c) { {"uuid": $c.uuid, "value": $c.value } })}' \  --header 'Authorization: Bearer $API_KEY'

This should return:

{  "id": "CA:B2:31:EE:E0:9E",  "characteristics": [    {      "uuid": "2A00",      "value": [        65,        112,        112,        108,        101,        32,        84,        86      ]    },    {      "uuid": "2A01",      "value": [        128,        2      ]    },    {      "uuid": "2A05",      "value": []    },    {      "uuid": "8667556C9A374C9184ED54EE27D90049",      "value": []    }  ]}

The syntax for this transform was admittedly more difficult, and thus a more advanced use of JSONata transforms might seem daunting. However, JSONata makes it really easy to experiment with transforms using their "JSONata Exerciser UI": try out the above example.

What if you want to call the GET /devices endpoint to fetch all of your devices, but apply a different transform per device type? For this scenario you could use a ternary expression such as:

type = "Bluetooth LE" ? { "id": id, "characteristics": $map(state.*.characteristics.*, function($c) { {"uuid": $c.uuid, "value": $c.value } })} : { "id": id, "reportedName": state.reported.name, "connectedBLEDevices": $keys(state.reported.statusConnections) }

This will apply one transform to Bluetooth LE types, and another to all the other (IP-based) types. However, this can get more complicated if you want to conditionally apply a third transform because JSONata does not support switch/case.

An Important Caveat#

It's important to realize that transforms only work on the JSON of a Device resource, not on the entire JSON response, which might contain metadata, paging data, etc. So when experimenting with JSONata, you need to paste in the JSON for a single resource in order to work out the transformation expression. This will then get applied to each resource contained in the endpoint's response.