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.
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.
Mathias wrote on :
Andrea: Good suggestion — a version that uses
xhr.responseType = 'json'
where possible, falling back toJSON.parse()
as needed, would be useful.Not all
responseType
-aware browsers supportresponseType = 'json'
, though, so your specific implementation may not be the most optimal.Anne wrote on :
You can test JSON support for
responseType
like this:(Will be
false
in Chrome.)Mathias wrote on :
Anne: Cool, thanks! It seems like the
reponseType
assignment should be wrapped in atry
-catch
, to avoid throwing errors in Safari 6. For the record, here’s what that would look like: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:Mathias wrote on :
erich: The
xhr.responseType
assignment must be wrapped in atry-catch
. See comment #4.MaxArt wrote on :
Too bad that if
json
is natively supported you are given no clue why a response is badly formatted, and havenull
instead. You can’t even try to re-parse the response sinceresponseText
is unavailable. You can check ifresponse === 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 manualJSON.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 :
Should this?
Be?
Mathias wrote on :
Ryan: No. It’s fine either way.
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 :
I try some options here, but always get “Something went wrong”. For testing, I used this link: https://maps.googleapis.com/maps/api/distancematrix/json?origins=Vancouver&destinations=San+Francisco&mode=driving&language=fr-FR I also tried to change
GET
toPOST
, but always the same. (I also changedata.ip
to for exampledata.destination_addresses
. Any idea?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.Ron wrote on :
The promises implementation does not work in IE11. You have to
resolve(JSON.parse(xhr.response));