I have been using Kendo UI for over a decade, and one of my major frustrations is that it does not work with complex JSON. This issue does not matter if you control the backend service endpoint, as you can format the JSON on the server; however, it can be an issue when working with external APIs, such as Azure Maps. Fortunately, Kendo UI provides other methods to hit the service endpoint to solve this tricky issue.



Kendo UI Expects JSON to be a Flattened Array of Objects

Kendo is very opinionated about how it expects JSON to be formatted when using a Kendo Datasource. By default, Kendo UI expects the JSON to be a flattened array of objects and must contain an outer array. If this array is absent, Kendo will throw a 'e.slice is not a function' error when consuming the Kendo Datasource. 

If the JSON returned from the server does not contain an array, an error will be thrown as the Kendo UI code tries to create a shallow copy of the array using the first element found in the JSON returned from the server.


Kendo UI Working JSON Example 

The following JSON works with Kendo UI as it contains a flattened array of objects. Note the square brackets '[]' at the beginning and end of the string, which create the outer array. Without this outer array in the JSON, Kendo UI will raise an 'e.slice is not a function' error unless there is additional logic.

The objects within the array, surrounded by curly braces '{}', contain data using name-value pairs. Additionally, Kendo UI needs this array of objects to be flattened, and the Kendo widgets will not work with any further nested arrays or objects.

The following is an example of a properly formatted JSON string that works with Kendo:


[
    {
        "freeformAddress": "Village Road & Lopez Road, Lopez Island, WA 98261",
        "lat": 48.5231179,
        "lon": -122.913293
    },
    {
        "freeformAddress": "Lopez Circle & Lopez Way, Hot Springs Village, AR 71909",
        "lat": 34.636804,
        "lon": -92.934058
    }
]

Issues When Working with Complex JSON and Kendo UI

The problem with Kendo UI's opinionated approach with JSON is that it can be challenging to work with JSON provided by external APIs. Well-constructed JSON is not limited to containing a flattened array of objects. Vendors are free to implement JSON as they see fit, and they often provide JSON in ways that will not work with Kendo UI's rigid JSON requirements. 

Kendo provides some additional data source configuration options, such as schema.data to point to the object containing a nested array—but this option does not always work for certain widgets and can't be used to point to multiple arrays in the server's JSON object. 

Kendo's schema.parse method can also be used to parse the response from the server; however, if the JSON does not adhere to the expected Kendo JSON requirements, it will raise an e.slice is not a function' error when you try to pass the response object to the parse function (parse: function(response)). This parse function is typically used to change the properties of the nested objects inside the JSON array and will not eliminate e.slice errors when the outer array is missing in the JSON.


Complex JSON Example 

The following JSON, returned from the Azure Maps Service, is too complex for Kendo UI. First, notice that the outer array (the square brackets '[]' at the beginning and end of this string) is missing. Second, this JSON is not flat and has several nested objects—note the results, address, and results.position elements.

While this is perfectly valid JSON, it will not work with the default configuration options with Kendo UI. If you try to consume this endpoint, Kendo will throw the dreaded 'e.slice is not a function' error. Even if this JSON had an outer array, this JSON would still not work with Kendo due to the complex nested objects.


{
    "summary": {
        "query": "lopez village",
        "queryType": "NON_NEAR",
        "queryTime": 26,
        "numResults": 3,
        "offset": 0,
        "totalResults": 3,
        "fuzzyLevel": 1
    },
    "results": [
        {
            "type": "Cross Street",
            "id": "KW81bePzSuMPDKHbvG2phA",
            "score": 0.9583277054894866,
            "matchConfidence": {
                "score": 0.9583277054894866
            },
            "address": {
                "streetName": "Village Road & Lopez Road",
                "municipality": "Lopez Island",
                "countrySecondarySubdivision": "San Juan",
                "countrySubdivision": "WA",
                "countrySubdivisionName": "Washington",
                "countrySubdivisionCode": "WA",
                "postalCode": "98261",
                "countryCode": "US",
                "country": "United States",
                "countryCodeISO3": "USA",
                "freeformAddress": "Village Road & Lopez Road, Lopez Island, WA 98261",
                "localName": "Lopez Island"
            },
            "position": {
                "lat": 48.5231179,
                "lon": -122.913293
            },
            "viewport": {
                "topLeftPoint": {
                    "lat": 48.52402,
                    "lon": -122.91465
                },
                "btmRightPoint": {
                    "lat": 48.52222,
                    "lon": -122.91194
                }
            }
        },
        {
            "type": "Cross Street",
            "id": "4GpqjYXs96UtP8UjRH3pUw",
            "score": 0.8975622642313136,
            "matchConfidence": {
                "score": 0.8975622642313136
            },
            "address": {
                "streetName": "Lopez Circle & Lopez Way",
                "municipality": "Marble",
                "countrySecondarySubdivision": "Saline",
                "countrySubdivision": "AR",
                "countrySubdivisionName": "Arkansas",
                "countrySubdivisionCode": "AR",
                "postalCode": "71909",
                "countryCode": "US",
                "country": "United States",
                "countryCodeISO3": "USA",
                "freeformAddress": "Lopez Circle & Lopez Way, Hot Springs Village, AR 71909",
                "localName": "Hot Springs Village"
            },
            "position": {
                "lat": 34.636804,
                "lon": -92.934058
            },
            "viewport": {
                "topLeftPoint": {
                    "lat": 34.6377,
                    "lon": -92.93515
                },
                "btmRightPoint": {
                    "lat": 34.6359,
                    "lon": -92.93296
                }
            }
        }
    ]
}

When Dealing With Kendo UI with Complex JSON, Use a Custom AJAX Function to Parse the Response and Construct a Flattened JSON Object

Thankfully, Kendo UI is flexible, and the solution is to create a custom AJAX function within a Kendo DataSource object. This custom AJAX function will consume the Azure Maps API and construct a flattened JSON object that plays well with Kendo.

In the example code below, I use a custom AJAX function within the transport.read section in the Kendo Datasource. This custom AJAX function invokes the Azure Maps API and passes a 'query' named parameter using the value typed into the location autocomplete field by the user.

In the AJAX success event, I use the Kendo UI options.success callback method and pass the response to my custom parseResponse function that parses the response to create a new flattened JSON object back to Kendo.

The custom parseResponse function constructs an empty array (jsonObj), loops through the JSON response, constructs a new JSON object, and populates it using the values from the server for each record. After this custom method creates the JSON object, I use the push JavaScript method to insert each object into the new JSON array (jsonObj). Finally, this custom method writes the new object to the console for debugging purposes and returns the new flattened object to the options.success method.

The options.success method then notifies the data source that the request has succeeded, and uses the values within the new JSON objects to populate the Kendo AutoComplete.

I also define the fields I use in the model.schema declaration. In this example, I use the freeFormAddress, the latitude (lat), and the longitude. However, this step is optional and is not necessary.

Please feel free to ask any questions by dropping me a line in the comments.

Click on the button below to see this example in action.


Code


JavaScript


$(document).ready(function(){
		
	// Azure Maps API URL
	var geoServiceUrl = "https://atlas.microsoft.com/search/address/json?subscription-key=<cfoutput>#azureMapsKey#</cfoutput>&typeahead=true&api-version=1&language=en-US&countrySet=US&view=Auto";

	function parseResponse(obj){

		// https://stackoverflow.com/questions/15009448/creating-a-json-dynamically-with-each-input-value-using-jquery
		// Instantiate the json objects
		jsonObj = [];

		// Loop through the items in the object
		for (var i = 0; i < obj.results.length; i++) {
			if (obj.results[i]) {
				// Get the data from the object
				var results = obj.results[i];// Results is an array in the json returned from the server
				// Create the struct. We need the latitute, longitude and the POI if it exists
				let jsonItems = {
					freeformAddress: results.address.freeformAddress,
					lat: results.position.lat,
					lon: results.position.lon
				};
				// Push the items into the new json object
				jsonObj.push(jsonItems);

				// console.log(results.address.freeformAddress);
				// console.log(results.position.lat);
				// console.log(results.position.lon);
			}
		}//..for
		// Write the object out for testing
		console.log(jsonObj);
		// And return it...
		return jsonObj;
	}

	var locationDs = new kendo.data.DataSource({
		transport: {
			read: function(options) {

				// Perform a custom the AJAX request to the Azure Maps API
				$.ajax({
					url: geoServiceUrl, // the URL of the API endpoint.
					data: { // Pass the value typed in to the form for the query parameter
						query: function(){
							return $("#location").data("kendoAutoComplete").value();
						},//..query
					},//..data
					dataType: "json", // Use json if the template is on the current server. If not, use jsonp for cross domain reads.
					success: function(result) {
						// If the request is successful, call the options.success callback
						options.success( parseResponse(result) );
					},
					error: function(error) {
						// If the request fails, call the options.error callback
						options.error(error);
					}
				});//ajax

			},//read
			schema: {
				model: {
					fields: {
						freeformAddress: {type: "string" },
						lat: {type: "string" },
						lon: {type: "string" }
					}//fields
				}//model
			}//schema
		},//transport
		cache: false,
		serverFiltering: true // without this argument, the autocomplete will not work and only fire the ajax request once
	});

	$("#location").kendoAutoComplete({
		minLength: 3,
		dataSource: locationDs, 
		dataTextField: "freeformAddress" // The widget is bound to the "freeformAddress" field
	});

});//document.ready

HTML


<table cellpadding="0" cellspacing="0" class="k-content">
	<tr>
		<td colspan="2">
			Azure Maps Search API
		</td>
	</tr>
	<tr>
		<td width="15%" align="right">
			<label>Location:</label>
		</td>
		<td width="*">
			<input id="location" name="location" style="width:500px" class="k-content"/>
		</td>
	</tr>
</table>

Further Reading


Note: This approach took me a long time to figure out, and I finally found this solution by reading the 'set read as a function' section in the Kendo DataSource Transport Read Documentation at https://docs.telerik.com/kendo-ui/api/javascript/data/datasource/configuration/transport.read.