Calling a REST Web Service (JSON) with Apex

January 6, 2010 Jeff Douglas

Jeff Douglas

Using JSON RESTful Web Services with Salesforce.com opens up your org to a number third-party integration opportunities (Google, Yahoo!, Flickr, bespoke, etc.). JSON support isn’t baked into the Force.com platform but Ron Hess at Salesforce.com has created a JSON parser which will do the heavy lifting for you.

Last month I wrote a blog post and example of how to call a REST Web Service with Apex that returns and consumes XML. It was my intention to do the same demo using JSON, however, I ran into a small sang. I couldn’t get the Apex JSONObject parser to work. I tried on and off for a couple of days but couldn’t beat it into submission. I checked around the twitter-verse and no one reported much success using the JSON parser with complex objects. I finally cried “uncle” and called Ron and asked for help. Ron was extremely responsive and over the course of a couple of days we worked worked through some of the parsing issues and finally updated the Google project with the changes.

I put together a small demo where you enter your address and the Apex code fetches the address and coordinates from the Google Maps . The service returns the data as a JSON object. You can run this example on my Developer Site.

To get started, you’ll need to download the JSONObject class and install it into a Developer org or Sandbox. Unfortunately there is no documentation for the parser so you have to extrapolate from the json.org website.

You’ll also need to sign up for a Google Maps API key in order to use their geocoding service. I would also recommend that you take a look at the docs for Google Maps geocoding service.

Here is the Controller for the demo. The interesting stuff is in the getAddress() and toGeoResult() methods. In getAddress(), the user-entered address is used to construct the URL for the GET call to the geocoding service. Make sure you properly encode the address or you may receive undesirable results returned from Google. One thing to point out is line #58. Google is returning a line feed in their JSON response which causes the JSON parser to choke. I simply replace all like feeds with spaces and that did the trick. Ron was going to look into making this change to the JSONObject class in the near future.

I was also having some problems with the geocoding service so I hard-coded the returned JSON object for testing. I checked around and it seems to be a common problem that the Google Maps API randomly returns 620 errors when overloaded. You might want to take a look at the JSON response returned for the hard-coded address. I will give you a little insight for the parsing process.

The toGeoResult() method parses the returned JSON response and populates the GeoResult object with the appropriate data. I chose this Google Maps example because it shows how to parse simple values, nested JSON objects and arrays. The coordinates for the address can either be returned as integers or doubles so I have to check each one.


public class RestDemoJsonController {

public String geoAddress {get;set;}
public String address {get;set;}
public String city {get;set;}
public String state {get;set;}
public Boolean useGoogle {get;set;}

// google api key
private String apiKey {get;set { apiKey = 'ABQIAAAAlI0DHB0p0WGX35GrKEAzQhTwZth5GdZI-P7ekoe_gyhfzl1yZhRAYdM-hb7aEWu30fGchcvGuwuUqg'; } }

// method called by the Visualforce page's submit button
public PageReference submit() {

if (address.length() == 0) {
ApexPages.addMessage(new ApexPages.message(ApexPages.severity.ERROR,'Address cannot be blank'));
}
if (city.length() == 0) {
ApexPages.addMessage(new ApexPages.message(ApexPages.severity.ERROR,'City cannot be blank'));
}
if (state.length() == 0) {
ApexPages.addMessage(new ApexPages.message(ApexPages.severity.ERROR,'State cannot be blank'));
}

if (!ApexPages.hasMessages())
geoAddress = getAddress(address,city,state);

return null;
}

// call the geocoding service
private String getAddress(String street, String city, String state) {

String json;

// hard-coded returned JSON response from Google
if (useGoogle) {
json = '{ "name": "1600 Amphitheatre Parkway, Mountain View, CA", "Status": { "code": 200, "request": "geocode" }, "Placemark": [ { "id": "p1", "address": "1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA", "AddressDetails": { "Accuracy" : 8, "Country" : { "AdministrativeArea" : { "AdministrativeAreaName" : "CA", "SubAdministrativeArea" : { "Locality" : { "LocalityName" : "Mountain View", "PostalCode" : { "PostalCodeNumber" : "94043" }, "Thoroughfare" : { "ThoroughfareName" : "1600 Amphitheatre Pkwy" } }, '+
' "SubAdministrativeAreaName" : "Santa Clara" } }, "CountryName" : "USA", "CountryNameCode" : "US" }}, "ExtendedData": { "LatLonBox": { "north": 37.4251466, "south": 37.4188514, "east": -122.0811574, "west": -122.0874526 } }, "Point": { "coordinates": [ -122.0843700, 37.4217590, 0 ] } } ]} ';

// call the geocoding service live
} else {

HttpRequest req = new HttpRequest();
Http http = new Http();
// set the method
req.setMethod('GET');
// generate the url for the request
String url = 'http://maps.google.com/maps/geo?q='+ EncodingUtil.urlEncode(street,'UTF-8')+',+'
+ EncodingUtil.urlEncode(city,'UTF-8')+',+'
+ EncodingUtil.urlEncode(state,'UTF-8')
+'&output=json&sensor=false&key='+apiKey;
// add the endpoint to the request
req.setEndpoint(url);
// create the response object
HTTPResponse resp = http.send(req);
// the geocoding service is returning a line feed so parse it out
json = resp.getBody().replace('n', '');

}

try {
JSONObject j = new JSONObject( json );
return toGeoResult(j).toDisplayString();
} catch (JSONObject.JSONException e) {
return 'Error parsing JSON response from Google: '+e;
}

}

// utility method to convert the JSON object to the inner class
private GeoResult toGeoResult(JSONObject resp) {

GeoResult geo = new GeoResult();

try {

geo.address = resp.getValue('Placemark').values[0].obj.getValue('address').str;
geo.k eys = resp.keys();
geo.name = resp.getString('name');
geo.statusCode = resp.getValue('Status').obj.getValue('code').num;

// set the coordinates - they may either be integers or doubles
geo.coordinate1 = resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[0].num != NULL ? resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[0].num.format() : resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[0].dnum.format();
geo.coordinate2 = resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[1].num != NULL ? resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[1].num.format() : resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[1].dnum.format();
geo.coordinate3 = resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[2].num != NULL ? resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[2].num.format() : resp.getValue('Placemark').values[0].obj.getValue('Point').obj.getValue('coordinates').values[2].dnum.format();

} catch (Exception e) {
// #fail
}

return geo;
}

// inner class
private class GeoResult {

public Set keys;
public Integer statusCode;
public String name;
public String coordinate1;
public String coordinate2;
public String coordinate3;
public String address;
public String toDisplayString() {
return address + ' ['
+ coordinate1 + ', '
+ coordinate2 + ', '
+ coordinate3 + '] - Status: '
+ statusCode;
}

}

}

The Visualforce page is fairly simple and presents the user with a form to enter their address. If the geocoding services is experiencing issues, the user can check “Use hard-coded Google JSON response?” and the Controller with use the hard-coded JSON response instead of making the GET call to the geocoding service. Once submitted, the address is processed and the outputPanel is rerendered with the resulting address and coordinates.













This example calls Google Map's geocoding REST service (JSON) with the address
you provide below.



Sometimes the geocoding services stops responding due to service availability. If you are receiving errors
with the returned JSON object, you can check the "Use hard-coded JSON response" to use a returned JSON
response hard-coded into the controller from Google's address.





Address






City






State







Use hard-coded Google JSON response?














Unit Testing

Writing unit tests for callouts can present a challenge. Scott Hemmeter has a really good article entitled Testing HTTP Callouts which should provide you with some useful techniques. You should also check out An Introduction to Apex Code Test Methods on the developerforce wiki.

Previous Article
Upcoming Salesforce.com Spring ‘10 Features

Jeff Douglas For those that don’t have time to weed through all 171 pages of the Spring ‘10 Release Notes, ...

Next Article
Performing Lookups and Transformations with Talend
Performing Lookups and Transformations with Talend

Ward Loving In this article, I’d like to explore the workhorse of the Talend Open Studio – the tMap compone...