StackExchange API on Salesforce!

As you probably already know, I am a huge fan of the Salesforce StackExchange. I recently learned that the StackExchange actually has an API exposed to pull information out of their system. I thought it would be fun to examine how we could pull some of that information into Salesforce. This article will focus on using the HTTP classes and the JSON classes to accomplish that.

HTTP Classes

The first thing we need to do is get the JSON back from the RESTful endpoint that the StackExchange API exposes. There are a handful of different endpoints exposed, but for this demo I will focus on the /users/{ids}/answers endpoint. This endpoint returns the answers the users in {ids} have posted. To obtain the information from this endpoint, I will use my user Id (605) and specify the site that I want to access by setting the site parameter to salesforce. The endpoint will eventually look like this: http://api.stackexchange.com/2.2/users/605/questions?order=desc&sort=activity&site=salesforce

To begin the process, we will instantiate an HTTP class. This class will use the send(HttpRequest) which will send an HTTPRequest class out and return an HTTPResponse class. The HTTPRequest class can be used to programmatically create HTTP requests like GET, POST, PUT, and DELETE. In this scenario, we will be using it to create a GET request. The HTTPResponse class is used to handle the HTTP response returned by the Http class.

Http httpQuestions = new Http();
HttpRequest requestQuestions = new HttpRequest();
requestQuestions.setEndpoint('http://api.stackexchange.com/2.2/users/605/questions?order=desc&sort=activity&site=salesforce');
requestQuestions.setMethod('GET');
requestQuestions.setHeader('Content-Type', 'application/json');
HTTPResponse responseQuestions = httpQuestions.send(requestQuestions);

The returned JSON looks like:

{
  "items": [
    {
      "tags": [
        "callout",
        "http",
        "httprequest",
        "spring14",
        "httpresponse"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 110,
      "accepted_answer_id": 32360,
      "answer_count": 1,
      "score": 6,
      "last_activity_date": 1397099443,
      "creation_date": 1397085310,
      "last_edit_date": 1397099443,
      "question_id": 32358,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/32358\/http-callout-problems-in-pre-release",
      "title": "HTTP Callout Problems in Pre-release"
    },
    {
      "tags": [
        "pre-release"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 224,
      "accepted_answer_id": 25486,
      "answer_count": 2,
      "score": 4,
      "last_activity_date": 1391032089,
      "creation_date": 1390247152,
      "question_id": 24668,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/24668\/writing-apex-in-spring-14-prerelease-org",
      "title": "Writing Apex in Spring '14 Prerelease Org"
    },
    {
      "tags": [
        "soql",
        "lookup"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 111,
      "accepted_answer_id": 25011,
      "answer_count": 1,
      "score": 4,
      "last_activity_date": 1390542679,
      "creation_date": 1390521475,
      "question_id": 25001,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/25001\/nested-soql-query-on-object-w-lookup-to-same-object",
      "title": "Nested SOQL Query On Object w\/ Lookup To Same Object"
    },
    {
      "tags": [
        "salesforce1-app"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 197,
      "accepted_answer_id": 23865,
      "answer_count": 2,
      "score": 4,
      "last_activity_date": 1389706625,
      "creation_date": 1389274614,
      "question_id": 23864,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/23864\/set-standard-objects-as-menu-item-in-salesforce1",
      "title": "Set Standard Objects as Menu Item in Salesforce1?"
    },
    {
      "tags": [
        "lookup",
        "page-layout",
        "create-records"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 71,
      "accepted_answer_id": 22462,
      "answer_count": 1,
      "score": 6,
      "last_activity_date": 1387298696,
      "creation_date": 1387298188,
      "last_edit_date": 1387298510,
      "question_id": 22460,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/22460\/modify-new-object-layout-through-lookup",
      "title": "Modify "New" Object Layout through Lookup"
    },
    {
      "tags": [
        "soql",
        "sosl"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 5596,
      "accepted_answer_id": 9029,
      "answer_count": 2,
      "score": 16,
      "last_activity_date": 1385919473,
      "creation_date": 1362021485,
      "question_id": 9028,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/9028\/soql-vs-sosl-which-one-to-use-and-when",
      "title": "SOQL vs SOSL - Which one to use and when?"
    },
    {
      "tags": [
        "visualforce"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 3340,
      "protected_date": 1382362080,
      "accepted_answer_id": 8140,
      "answer_count": 1,
      "score": 38,
      "last_activity_date": 1382351724,
      "creation_date": 1360161554,
      "last_edit_date": 1360249961,
      "question_id": 8139,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/8139\/difference-between-the-multiple-messaging-options-in-visualforce",
      "title": "Difference between the multiple messaging options in Visualforce?"
    },
    {
      "tags": [
        "leads",
        "lead-conversion"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 214,
      "accepted_answer_id": 17800,
      "answer_count": 1,
      "score": 6,
      "last_activity_date": 1381175720,
      "creation_date": 1381173961,
      "question_id": 17799,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/17799\/multiple-contacts-per-lead-possible",
      "title": "Multiple Contacts Per Lead - Possible?"
    },
    {
      "tags": [
        "ide"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 1725,
      "protected_date": 1377220053,
      "accepted_answer_id": 15659,
      "answer_count": 2,
      "score": 62,
      "last_activity_date": 1379438764,
      "creation_date": 1376944562,
      "last_edit_date": 1376949857,
      "question_id": 15654,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/15654\/force-com-ide-still-officially-supported",
      "title": "Force.com IDE - Still Officially Supported?"
    },
    {
      "tags": [
        "apex",
        "unit-test",
        "batch",
        "api-27"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 1628,
      "accepted_answer_id": 8978,
      "answer_count": 1,
      "score": 7,
      "last_activity_date": 1368082166,
      "creation_date": 1361912702,
      "last_edit_date": 1365153475,
      "question_id": 8959,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/8959\/no-mass-mail-permission-error-from-unit-test-on-singlemailmessage",
      "title": "NO_MASS_MAIL_PERMISSION Error from Unit Test on SingleMailMessage"
    },
    {
      "tags": [
        "developer-console",
        "api-27"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 1145,
      "protected_date": 1362776429,
      "accepted_answer_id": 9191,
      "answer_count": 1,
      "score": 16,
      "last_activity_date": 1365700479,
      "creation_date": 1362156576,
      "last_edit_date": 1362166843,
      "question_id": 9131,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/9131\/developer-console-issues-in-chrome-spring-13",
      "title": "Developer Console - Issues in Chrome Spring '13?"
    },
    {
      "tags": [
        "deployment",
        "migration-tool"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 419,
      "accepted_answer_id": 10187,
      "answer_count": 3,
      "score": 11,
      "last_activity_date": 1364654401,
      "creation_date": 1364565408,
      "last_edit_date": 1364565843,
      "question_id": 10170,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/10170\/deploy-to-production-cancel-deploy-after-test-failure",
      "title": "Deploy to Production - Cancel Deploy After Test Failure"
    },
    {
      "tags": [
        "apex",
        "approval-process"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 575,
      "accepted_answer_id": 8152,
      "answer_count": 2,
      "score": 3,
      "last_activity_date": 1363279254,
      "creation_date": 1360170102,
      "last_edit_date": 1360174891,
      "question_id": 8150,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/8150\/programmatically-stopping-approval-process-email",
      "title": "Programmatically Stopping Approval Process Email"
    },
    {
      "tags": [
        "ide"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 123,
      "answer_count": 1,
      "score": 1,
      "last_activity_date": 1362633144,
      "creation_date": 1362585799,
      "question_id": 9278,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/9278\/force-com-ide-easy-push-of-single-file-through-single-key-combo",
      "title": "Force.com IDE - Easy Push of Single File Through Single Key Combo?"
    },
    {
      "tags": [
        "apex",
        "unit-test",
        "developer-console"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 368,
      "accepted_answer_id": 9151,
      "answer_count": 2,
      "score": 6,
      "last_activity_date": 1362265094,
      "creation_date": 1362162793,
      "question_id": 9136,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/9136\/force-a-unit-test-to-run-immediately-inside-salesforce-web-ui",
      "title": "Force a Unit Test to run immediately inside Salesforce Web UI?"
    },
    {
      "tags": [
        "deployment",
        "migration-tool",
        "package"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 539,
      "accepted_answer_id": 9057,
      "answer_count": 1,
      "score": 6,
      "last_activity_date": 1362061078,
      "creation_date": 1362060569,
      "question_id": 9056,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/9056\/sfretrieve-gets-a-file-but-sfdeploy-cant-find-it-for-the-deploy",
      "title": "sf:retrieve gets a file, but sf:deploy can't find it for the deploy?"
    },
    {
      "tags": [
        "metadata-api",
        "migration-tool"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 713,
      "accepted_answer_id": 8791,
      "answer_count": 1,
      "score": 5,
      "last_activity_date": 1361459880,
      "creation_date": 1361457257,
      "question_id": 8783,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/8783\/full-list-of-all-accessible-types-through-metadata-api",
      "title": "Full List of All Accessible Types Through Metadata Api"
    },
    {
      "tags": [
        "site.com"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 353,
      "bounty_amount": 50,
      "accepted_answer_id": 8022,
      "answer_count": 1,
      "score": 7,
      "last_activity_date": 1359983695,
      "creation_date": 1358274012,
      "last_edit_date": 1359745295,
      "question_id": 7264,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/7264\/site-com-incorrectly-escaping-image-urls",
      "title": "Site.com - Incorrectly Escaping Image URLs"
    },
    {
      "tags": [
        "deployment",
        "migration-tool",
        "migration"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 190,
      "accepted_answer_id": 7982,
      "answer_count": 1,
      "score": 3,
      "last_activity_date": 1359756293,
      "creation_date": 1359745845,
      "question_id": 7971,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/7971\/ant-migration-tool-order-of-package-xml-file-matter",
      "title": "Ant Migration Tool - Order of Package.xml File Matter?"
    },
    {
      "tags": [
        "search"
      ],
      "owner": {
        "reputation": 9143,
        "user_id": 605,
        "user_type": "registered",
        "accept_rate": 95,
        "profile_image": "http:\/\/i.stack.imgur.com\/2xQqK.png?s=128&g=1",
        "display_name": "Jesse Altman",
        "link": "http:\/\/salesforce.stackexchange.com\/users\/605\/jesse-altman"
      },
      "is_answered": true,
      "view_count": 610,
      "accepted_answer_id": 7641,
      "answer_count": 1,
      "score": 1,
      "last_activity_date": 1359042717,
      "creation_date": 1358523859,
      "question_id": 7417,
      "link": "http:\/\/salesforce.stackexchange.com\/questions\/7417\/customizing-global-search",
      "title": "Customizing Global Search"
    }
  ],
  "has_more": false,
  "quota_max": 300,
  "quota_remaining": 299
}

Inner Class

Now in order to get the JSON that is returned into a usable form in Apex, we need to make SObjects. To do that, I created these classes.

public class SFSEQuestions{
    public List<SFSEQuestion> items;
        
    public SFSEQuestions(){
    }
        
    public List<SFSEQuestion> getItems(){
        return items;
    }
    public void setItems(List<SFSEQuestion> items){
        this.items = items;
    }
}

public class SFSEQuestion{
    public String title;
    public String link;
    public Integer votes;

    public SFSEQuestion(){
    }
        
    public Boolean isComplete(){
        return title != null && link != null && votes != null;
    }
        
    public String getTitle(){
        return title;
    }
    public void setTitle(String title){
        this.title = title;
    }
    public String getLink(){
        return link;
    }
    public void setLink(String link){
        this.link = link;
    }
    public Integer getVotes(){
      	return votes;
    }
    public void setVotes(Integer votes){
       	this.votes = votes;
    }
}

JSON.deserialize

One of the options available for setting the data from the endpoint to the SObject is to use the JSON class. The deserialize(String, System.Type) method deserializes the specified JSON string into an Apex object of the specified type. In our scenario all we need to do is pass the JSON string from responseQuestions.getBody() into the method and specify that it returns as an SFSEQuestions class. The deserialize method returns as an SObject so we must cast it to a SFSEQuestions class.

List<SFSEQuestion> questions = new List<SFSEQuestion>();
SFSEQuestions jsonQuestions = (SFSEQuestions)JSON.deserialize(responseQuestions.getBody(), SFSEQuestions.class);
questions = jsonQuestions.getItems();

JSON Parsing

The other option we have available is to use a JSONParser class to manually parse the JSON into the specified classes. First, we need to create the JSONParser using the JSON.createParser(String) method. We will then loop over the values using the nextToken() method. You can compare that token using the currentToken() method to the JSONToken enum. Once you find the field you are looking for (with the if statements), you will iterate to the nextToken and then use one of the several methods available to return the proper data type. In our scenario, those methods are getText() and getIntegerValue().

List<SFSEQuestion> questions = new List<SFSEQuestion>();
JSONParser parser = JSON.createParser(responseQuestions.getBody());
SFSEQuestion question = new SFSEQuestion();
while (parser.nextToken() != null) {
    if((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'score')) {
        parser.nextToken();
        question.setVotes(parser.getIntegerValue());
    }
                
    if((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'link')) {
        parser.nextToken();
        question.setLink(parser.getText());
    }
                
    if((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'title')) {
        parser.nextToken();
        question.setTitle(parser.getText());
    }
                                
    if(question.isComplete()){
        questions.add(question);
        question = new SFSEQuestion();
    }
}

Conclusion

Parsing JSON in Salesforce has several options available to it. In most scenarios, using the JSON class to just deserialize the data into SObjects is much simpler. There are even more methods available to deserialize JSON even if you don’t know the Type that will be returned. However, there will always be the option to manually parse the JSON if needed. Good luck and have some fun playing around with the StackExchange API!

2 Responses to “StackExchange API on Salesforce!”

  1. Peter Churchill
    April 23, 2014 at 11:59 am #

    One thing I would add re using JSON.deserialize is the JSON to APEX tool at http://json2apex.herokuapp.com/

    It will take your JSON and make it into an Apex class you can then deserialize into…real time saver.

    • April 24, 2014 at 6:53 pm #

      That is a really cool tool! Thanks for sharing. I didn’t know that existed.

Leave a Comment