Creating a custom global describe API using @RestResource

A colleague is working on a client that needs to know all the SObject names and all the field names within those SObjects. The Apex describe APIs provide this information but also a lot of other information that is not required in this case. So it is worth doing work at the server-side to cut down the information to only what is required by the client.

(In the org in question, the 300 SObjects produce JSON output of 800 kB, well below the 3 MB governor limit on HTTP responses.)

Salesforce’s @RestResource mechanism makes doing this pretty easy. The code below transfers the required information into instances of simple Apex classes, sorts the data based on label first then API name second, and then leaves it up to the platform to serialise those as JSON:

@RestResource(urlMapping='/v1/describe')
global with sharing class DescribeRest {

    global class Sob implements Comparable {
        
        public String sobLabel;
        public String sobApi;
        public Field[] sobFields;
        
        Sob(SObjectType t) {
            DescribeSObjectResult r = t.getDescribe();
            sobLabel = r.getLabel();
            sobApi = r.getName();
            sobFields = new Field[] {};
            for (SObjectField f : r.fields.getMap().values()) {
                sobFields.add(new Field(f));
            }
            sobFields.sort();
        }
        
        public Integer compareTo(Object o) {
            Sob that = (Sob) o;
            if (this.sobLabel < that.sobLabel) return -1;
            else if (this.sobLabel > that.sobLabel) return 1;
            else {
                if (this.sobApi < that.sobApi) return -1;
                else if (this.sobApi > that.sobApi) return 1;
                else return 0;
            }
        }
    }
    
    global class Field implements Comparable {
        
        public String label;
        public String api;
        
        Field(SObjectField f) {
            DescribeFieldResult r = f.getDescribe();
            label = r.getLabel();
            api = r.getName();
        }
        
        public Integer compareTo(Object o) {
            Field that = (Field) o;
            if (this.label < that.label) return -1;
            else if (this.label > that.label) return 1;
            else {
                if (this.api < that.api) return -1;
                else if (this.api > that.api) return 1;
                else return 0;
            }
        }
    }
    
    @HttpGet
    global static Sob[] get() {
        
        Sob[] sobs = new Sob[] {};
        for (SObjectType t : Schema.GetGlobalDescribe().values()) {
            sobs.add(new Sob(t));
        }
        sobs.sort();
        
        return sobs;
    }
}

When accessed using /services/apexrest/cveep/v1/describe.json, this produces JSON (formatted here to better illustrate the structure) taking about 10ms per object at the server-side:

[
    {
        "sobLabel":"Absence",
        "sobFields":[
            {"label":"Absence","api":"Name"},
            {"label":"Absence Type","api":"Type__c"},
            {"label":"Claim","api":"Claim__c"},
            ...
        ],
        "sobApi":"Absence__c"
    },
    ....
]

Now that the governor limits have been removed on describe calls the first limit that will be hit is probably the 3 MB response size limit.

Cool data tables using @RestResource, AngularJS and trNgGrid

I have an AngularJS application that shows tables of data using:

  • an Apex class that does dynamic SOQL and populates instances of a simple Apex class that are serialised to the client as JSON via the @RestResource annotation
  • the client side is AngularJS that pretty much just passes the JSON data through to a page template
  • the presentation work is all done by the excellent trNgGrid component and Bootstrap styling

The Apex code is clean and simple:

@RestResource(urlMapping='/report')
global without sharing class ReportRest {
    global class Claim {
        public String employeeName;
        public String department;
        public String reportsTo;
        public String claimNumber;
        public String status;
        public String leaveType;
        public Date startDate;
        public Date endDate;
        Claim(SObject c) {
            SObject e = c.getSObject('Employee__r');
            employeeName = (String) e.get('Name');
            department = (String) e.get('Department');
            SObject r = e.getSObject('ReportsTo');
            reportsTo = r != null ? (String) r.get('Name') : null;     
            claimNumber = (String) c.get('Name');
            status = (String) c.get('Status__c');
            leaveType = (String) c.get('LeaveType__c');
            startDate = (Date) c.get('StartDate__c');
            endDate = (Date) c.get('EndDate__c');
        }
    }
    @HttpGet  
    global static Claim[] get() {
        Claim[] claims = new Claim[] {};
        String soql = ...;
        for (SObject sob : Database.query(soql)) {
            claims.add(new Claim(sob));
        }
        return claims;
    }
}

and the trNgGrid markup is even more impressive:

<table tr-ng-grid="tr-ng-grid" class="table table-condensed" items="items"
      order-by="orderBy" order-by-reverse="orderByReverse">
  <thead>
    <tr>
      <th field-name="employeeName"/>
      <th field-name="department"/>
      <th field-name="reportsTo"/>
      <th field-name="claimNumber"/>
      <th field-name="status"/>
      <th field-name="leaveType"/>
      <th field-name="startDate" display-format="longDate" display-align="right"/>
      <th field-name="endDate" display-format="longDate" display-align="right"/>
    </tr>
  </thead>
</table>

You just define the column headers and trNgGrid generates the rows from the JSON data array (called “items” here). The resulting table has column sorting and column searching and other features can be enabled too. As it is written in AngularJS, it leverages AngularJS features such as filters (“longDate” here) for custom formatting.

What is great about this arrangement is that there is no tedious coding involved: all the code serves a purpose and the grunt work is handled by the frameworks. It also scores high on “ease of modification”: an extra column only takes a few minutes to add.

(Contrast this with the JavaScript required in e.g. Connecting DataTables to JSON generated by Apex.)

Here is a screen shot from the real application (with different columns):

TrNgGrid

An @RestResource Apex class that returns multiple JSON formats

The simplest way to write an @RestResource class is to return Apex objects from the @Http methods and leave it up to the platform to serialize these objects as JSON (or XML):

@RestResource(urlMapping='/report/*')
global without sharing class ReportRest {

    public class MyInnerClass {
        public String name;
        public Integer number;
    }

    @HttpGet  
    global static MyInnerClass get() {
        MyInnerClass instance = new MyInnerClass();
        ...
        return instance;
    }
}

This also allows tests to be written that don’t have to deserialize as they can just reference the class instances directly. But the approach imposes these limitations:

  • The response JSON is fixed and determined by the returned classes and their fields so responses that vary depending on the URL requested can’t be produced
  • Error conditions typically get handled by adding error fields to the response object rather than by returning a status code other than 200 and separate error information

Here is an alternate pattern that is a bit more work but in my experience meets the needs of client-side MVC applications (AngularJS in my case) better. The class returns two different JSON formats (depending on the part of the URL after “/report/”):

@RestResource(urlMapping='/report/*')
global without sharing class ReportRest {

    public class Day {
        public Date date;
        public Integer hours;
    }
    
    public class Employee {
        public String name;
        public Day[] approved = new Day[] {};
    }
    
    public class Claim {
        public String employeeName;
        public String claimNumber;
    }
 
    @HttpGet  
    global static void get() {
        RestResponse res = RestContext.response;
        if (res == null) {
            res = new RestResponse();
            RestContext.response = res;
        }
        try {
            res.responseBody = Blob.valueOf(JSON.serialize(doGet(extractReportId())));
            res.statusCode = 200;
        } catch (EndUserMessageException e) {
            res.responseBody = Blob.valueOf(e.getMessage());
            res.statusCode = 400;
        } catch (Exception e) {
            res.responseBody = Blob.valueOf(
                    String.valueOf(e) + '\n\n' + e.getStackTraceString()
                    );
            res.statusCode = 500;
        }
    }
    
    private static Object doGet(String reportId) {
        if (reportId == 'ac') {
            return absenceCalendarReport();
        } else if (reportId == 'al') {
            return absenceListReport();
        } else if (reportId == 'dl') {
            return disabilityListReport();
        } else {
            throw new EndUserMessageException(reportId + ' not implemented');
        }
    }
    
    private static Employee[] absenceCalendarReport() {
        Employee[] employees = new Employee[] {};
        ...
        return employees;
    }
    
    private static Claim[] absenceListReport() {
        Claim[] claims = new Claim[] {};
        ...
        return claims;
    }
    
    private static Claim[] disabilityListReport() {
        Claim[] claims = new Claim[] {};
        ...
        return claims;
    }
    
    private static String extractReportId() {
        String[] parts = RestContext.request.requestURI.split('\\/');
        String lastPart = parts[parts.size() - 1];
        Integer index = lastPart.indexOf('?');
        return index != -1 ? lastPart.substring(0, index) : lastPart;
    }
}

Apex classes are still used to represent the returned data but are explicitly serialized using a JSON.serialize call. As the overall response is being explicitly built, the returned status code can be set allowing the client side to vary its logic depending on that status code. In this example error information – intended to be shown to an end user as signalled by the EndUserMessageException custom exception or unintended and so including a stack trace – is returned as plain text that can be directly shown to the end user.

Creating an Attachment SObject complete with its body using the REST API from Java

Exporting the body of an Attachment using the REST API is fairly straightforward: the “Body” field of the Attachment JSON contains the URL to GET the raw bytes for the Attachment body from. But it took me a while to get the importing of an Attachment and its body to work because PATCH or POST to the equivalent body URL is not allowed.

This documentation Insert or Update Blob Data explains generically what needs to be done. But it took me a bit of time to work out how to do this in Java using the Apache HttpClient. Here is a stripped down version of what I ended up with that might help you if you are writing similar Java code:

package com.claimvantage.ant;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.PartBase;

public class AttachmentExample {
    
    private class JsonPart extends PartBase {
        
        private byte[] bytes;
        
        public JsonPart(String name, String json) throws IOException {
            super(name, "application/json", "UTF-8", null);
            this.bytes = json.getBytes("UTF-8");
        }
        
        @Override
        protected void sendData(OutputStream os) throws IOException {
            os.write(bytes);
        }

        @Override
        protected long lengthOfData() throws IOException {
            return bytes.length;
        }
    }
    
    private String baseUrl;     // Initialization not shown here
    private String sessionId;   // Initialization not shown here
    
    /**
     * Create attachment SObject from its JSON populating its Body from a file at the same time.
     */
    public void createAttachment(String attachmentJson, File attachmentFile) throws Exception {
        
        PostMethod post = new PostMethod(baseUrl + "/services/data/v23.0/sobjects/Attachment");
        post.setRequestHeader("Authorization", "OAuth " + sessionId);
        Part[] parts = new Part[] {
                new JsonPart("Json", attachmentJson),
                new FilePart("Body", attachmentFile)
                };
        post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));
        try {
            new HttpClient().executeMethod(post);
            if (post.getStatusCode() == HttpStatus.SC_CREATED) {
                // Logic for OK
            } else {
                // Error handling logic
            }
        } finally {
            post.releaseConnection();
        }
    }
}