Mathias Bynens

Loading JSON-formatted data with Ajax and xhr.responseType='json'

Published · tagged with DOM, HTTP, JavaScript

This post explains a hidden gem in the XMLHttpRequest standard that simplifies the process of fetching and parsing JSON data through Ajax.

JSON & JSON-P

A common way to offer server-generated data to browsers so that it can be used in client-side JavaScript is by formatting the data as JSON, and making it accessible through its own URL. For example:

$ curl 'https://mathiasbynens.be/demo/ip'
{"ip":"173.194.65.100"}

To make it easy to use this data in client-side JavaScript, most API endpoints offer a JSON-P version too:

$ curl 'https://mathiasbynens.be/demo/ip?callback=foo'
foo({"ip":"173.194.65.100"})

JSON-P allows you to use JSON-formatted data directly in JavaScript without having to programmatically parse it first. In other words, JSON-P is valid JavaScript, which allows you to do:

<script>
window.hollaback = function(data) {
alert('Your public IP address is: ' + data.ip);
};
</script>
<script src="https://mathiasbynens.be/demo/ip?callback=hollaback"></script>

Some APIs don’t offer JSON-P, though, and in some situations it’s preferable to load the raw JSON data through Ajax, then parse the serialized data string using JSON.parse() (or its polyfill for older browsers). This can be done as follows:

var getJSON = function(url, successHandler, errorHandler) {
var xhr = typeof XMLHttpRequest != 'undefined'
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP');
xhr.open('get', url, true);
xhr.onreadystatechange = function() {
var status;
var data;
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-readystate
if (xhr.readyState == 4) { // `DONE`
status = xhr.status;
if (status == 200) {
data = JSON.parse(xhr.responseText);
successHandler && successHandler(data);
} else {
errorHandler && errorHandler(status);
}
}
};
xhr.send();
};

getJSON('https://mathiasbynens.be/demo/ip', function(data) {
alert('Your public IP address is: ' + data.ip);
}, function(status) {
alert('Something went wrong.');
});

The above code works in pretty much any relevant browser, including IE6.

So far, I probably haven’t told you anything you didn’t already know. But here comes the good part. Thanks to a ‘hidden’ (not widely known) gem in the XMLHttpRequest standard, this code can be simplified a bit!

Enter xhr.responseType = 'json'

Each XMLHttpRequest instance has a responseType property which can be set to indicate the expected response type. When the property is set to the string 'json', browsers that support this feature automatically handle the JSON.parse() step for you. Using this feature, the above example can be written more elegantly:

var getJSON = function(url, successHandler, errorHandler) {
var xhr = typeof XMLHttpRequest != 'undefined'
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP');
xhr.open('get', url, true);
xhr.responseType = 'json';
xhr.onreadystatechange = function() {
var status;
var data;
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-readystate
if (xhr.readyState == 4) { // `DONE`
status = xhr.status;
if (status == 200) {
successHandler && successHandler(xhr.response);
} else {
errorHandler && errorHandler(status);
}
}
};
xhr.send();
};

getJSON('https://mathiasbynens.be/demo/ip', function(data) {
alert('Your public IP address is: ' + data.ip);
}, function(status) {
alert('Something went wrong.');
});

Check out the demo. Note that when the responseType is set to 'json', xhr.response must be used instead of xhr.responseText. When the browser fails to parse the response as JSON, null is returned (instead of throwing an error).

Since this code only works in modern browsers with xhr.responseType = 'json' support anyway, we might as well get rid of the code paths for legacy browsers:

var getJSON = function(url, successHandler, errorHandler) {
var xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
successHandler && successHandler(xhr.response);
} else {
errorHandler && errorHandler(status);
}
};
xhr.send();
};

getJSON('https://mathiasbynens.be/demo/ip', function(data) {
alert('Your public IP address is: ' + data.ip);
}, function(status) {
alert('Something went wrong.');
});

Much better, no? This is what the future looks like :) It gets even better when combined with JavaScript promises:

var getJSON = function(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
resolve(xhr.response);
} else {
reject(status);
}
};
xhr.send();
});
};

getJSON('https://mathiasbynens.be/demo/ip').then(function(data) {
alert('Your public IP address is: ' + data.ip);
}, function(status) {
alert('Something went wrong.');
});

Browser support

xhr.responseType = 'json' has only been in the spec since December 2011, and as of March 2016, Firefox ≥ 10 (Gecko), Chrome/Chromium ≥ 31, Opera (even the old Presto-based Opera 12!), Microsoft Edge, and Safari/WebKit all support it.

About me

Hi there! I’m Mathias. I work on Chrome DevTools and the V8 JavaScript engine at Google. HTML, CSS, JavaScript, Unicode, performance, and security get me excited. Follow me on Twitter, Mastodon, and GitHub.

Comments

Andrea Giammarchi wrote on :

Nice one as usual, but I wonder if the example should not be slightly better for the IE case you consider in any case.

var getJSON = function(url, successHandler, errorHandler) {
var xhr = typeof XMLHttpRequest != 'undefined'
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP');
var responseTypeAware = 'responseType' in xhr;
xhr.open('get', url, true);
if (responseTypeAware) {
xhr.responseType = 'json';
}
xhr.onreadystatechange = function() {
var status = xhr.status;
var data;
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-readystate
if (xhr.readyState == 4) { // `DONE`
if (status == 200) {
successHandler && successHandler(
responseTypeAware
? xhr.response
: JSON.parse(xhr.responseText)
);
} else {
errorHandler && errorHandler(status);
}
}
};
xhr.send();
};

wrote on :

Andrea: Good suggestion — a version that uses xhr.responseType = 'json' where possible, falling back to JSON.parse() as needed, would be useful.

Not all responseType-aware browsers support responseType = 'json', though, so your specific implementation may not be the most optimal.

Anne wrote on :

You can test JSON support for responseType like this:

x.responseType = "json"
var supportsJson = "response" in x && x.responseType == "json"

(Will be false in Chrome.)

wrote on :

Anne: Cool, thanks! It seems like the reponseType assignment should be wrapped in a try-catch, to avoid throwing errors in Safari 6. For the record, here’s what that would look like:

var supportsJSON = (function() {
if (typeof XMLHttpRequest == 'undefined') {
return false;
}
var xhr = new XMLHttpRequest();
xhr.open('get', '/', true);
try {
// some browsers throw when setting `responseType` to an unsupported value
xhr.responseType = 'json';
} catch(error) {
return false;
}
return 'response' in xhr && xhr.responseType == 'json';
}());

I’ve created a test case for all the available responseType values, added feature tests to Modernizr, and submitted reference tests to the W3C’s Web Platform Tests repository.

erich wrote on :

Anne: Actually you can just do ( xhr.responseType = responseType) !== xhr.responseType, might be implemented like this:

function getAjax( method, uri, onloadcallback, options ) {
var xhr = new XMLHttpRequest,
responseType = ( options || ( options = { } ) ).responseType;
xhr.open( method, uri, !! onloadcallback );
if ( responseType ) {
if ( ( xhr.responseType = responseType) !== xhr.responseType ) {
switch ( responseType.toLowerCase( ) ) {
case 'json':
if ( onloadcallback ) {
var origonload = onloadcallback;
onloadcallback = function ( evt ) { this.response = JSON.parse( this.responseText ); return origonload( evt ); };
} else
onloadcallback = function ( evt ) { this.response = JSON.parse( this.responseText ); };
break;
default:
throw responseType + ' is not a supported responseType';
}
}
}
if ( onloadcallback )
xhr.addEventListener( 'load', onloadcallback );
return xhr;
}

MaxArt wrote on :

Too bad that if json is natively supported you are given no clue why a response is badly formatted, and have null instead. You can’t even try to re-parse the response since responseText is unavailable. You can check if response === null, but "null" is a valid JSON too after all, and can have a meaning…

Andrea: That’s kind of how I implemented it for every browser (even down to IE7 when json2.js is loaded).

Tester wrote on :

Is there performance difference between the browser’s built-in JSON.parse and manual JSON.parse?

4esn0k wrote on :

It seems Opera 12 does not support HTML documents, so xhr.responseType = 'document' is not supported here fully…

Gagan wrote on :

Very well explained.

Is this available as an npm module (to use in browsers)? If not it would be nice if you put it as module a) for all browsers and b) for modern browsers.

I use the function shown in MDN’s “Getting started with Ajax”.

Ryan P.C. McQuen wrote on :

Mathias: I realize they both work, I was just wondering if double equals was chosen for any specific reason? Since triple equals is usually better.

Jean-Claude wrote on :

Jean-Claude: This is because that JSON document isn’t served with an Access-Control-Allow-Origin HTTP header that whitelists your origin. By default, loading cross-domain resources through Ajax is not allowed.

Leave a comment

Comment on “Loading JSON-formatted data with Ajax and xhr.responseType='json'

Your input will be parsed as Markdown.