5

Background

I'm developing some generics apex web service (Rest). The response should be an standardised json schema like this:

{
 "res": true,
 "message": "Records Upserted Ok",
 "data" : {ANYTHING}
}  

Where data attribute could be any result

sample 1 of data:

{
 "success" : [{"salesforceId":"XXXX","externalId":"ZZZZ"}],
 "errors" : [{"externalId":"YYYY","error":"Price field is required"}],
}

sample 2 of data:

{ "queryData": [
   {"id":"XXXXX","name":"Messi"}, 
   {"id":"ZZZZZ","name":"Kun"}
  ]
}

Approach

I've created a response helper class like this:

 //abstract basic class  (I have this )
 abstract class JSONResponseBase {

    public Boolean res;
    public String message;    
    // special methods here 

 }  

 global class JSONResponseMap extends JSONResponseBase {

        public Map<String,Object> data;

        public JSONResponseMap(Boolean res, String message, Map<String,Object> data){
            super(res, message);
            this.data = data;
        }
    }

Therefore, my web services have always this code:

@RestResource(urlMapping='/MyWS/*')
global class AccountCustomWS {
    @HttpPost
    global static JSONResponseMap upsertAccount() {

        JSONResponseMap res = new JSONResponseMap();

        // my code here

        return res;
    }

Problem

When I try to save the webservice class I'm getting this compilation error:

Invalid type for Http* method: JSONResponseMap.Map.Object (Line: 5)

I've noticed that if I change the data attribute to map<String, String> it works ok

Question

  1. Am I missing something?
  2. Is this a limitation (un-documented) of apex rest web service?
  3. What would be the best workaround to response a generic data attr?

2 Answers 2

3

First, I don't know of any explicit documentation about what the framework will accept as serializable signatures. So the answer to your question 2 is I think yes. Certainly SObjects, Lists, Maps and bean-like classes with definite field types work.

This class:

@RestResource(urlMapping='/MyWS/*')
global class AccountCustomWS {

    global interface Anything {
    }

    private class MyAnything implements Anything {
        public String name = 'value';
    }

    @HttpGet
    global static Map<String, Anything> doGet() {
        return new Map<String, Anything>{'any' => new MyAnything()};
    }
}

will compile and produces this JSON:

{"any":{"name":"value"}}

in response to a GET that ends with .json. So it appears that the compiler and runtime can be fooled by using an empty interface in place of Object and this approach may meet your needs and so is a possible answer to your question 3. But how far that can be taken (and how guaranteed the support is over time) is open to question. Generally you are safer following the KISS principle with Apex.

Interestingly, if you ask for XML with the same class (e.g no .json extension) you get this error:

Unsupported return type for XML: MAP<String,AccountCustomWS.Anything>
5
  • Thank you for your reply, I'm surprise how an empty interface can emulate a generic Object, really cleaver! I'm going to test your approach to see how deep can the framework resolve Mar 25, 2014 at 14:33
  • @MartinBorthiry Good to persue a bit out of academic interest, but given the lack of documentation (=commitment?), risky to use at any scale.
    – Keith C
    Mar 25, 2014 at 14:52
  • @KeithC: This is really cool. But the only concern is the support for this workaround.
    – Sam
    Jul 17, 2015 at 7:45
  • @Mugambo In theory changes introduced by Salesforce are versioned so if Salesforce did introduce a breaking change the old code would still work. A good thing is that this pattern avoids a compilation error so if they did make an unversioned breaking change that caused a run-time failure hopefully the compilation would start to fail too in the hammer process.
    – Keith C
    Jul 17, 2015 at 7:52
  • @KeithC: That was good read. So in theory we are safe. :)
    – Sam
    Jul 17, 2015 at 8:18
-2

As far as my understanding, If you would be familiar with Webservices, we may only return String values as text, XML or JSON as a return type of Webservice methods not a Class / Object itself.

To deal with your situation, just place the following code for AccountCustomWS:

@RestResource(urlMapping='/MyWS/*')
global class AccountCustomWS {
    @HttpPost
    global static String upsertAccount() {

        JSONResponseMap res = new JSONResponseMap();

        // my code here

        return JSON.serialize(res);
    }
}

And at the receiving end, to make sure you get the object representation back. Do the following:

JSONResponseMap jrm = (JSONResponseMap) JSON.desialize (upsertAccount(), Type.forName('JSONResponseMap.class'));

You may also do it like:

JSONResponseMap jrm = (JSONResponseMap) JSON.desialize (upsertAccount(), JSONResponseMap.class);

And use the jrm reference. In this reference you would get the object representation which was serialized by your Webservice method.

2
  • Thank you for your reply, but, you can't return a String that has a json, because this will be scaped, return a double escaped json e.g: "{\"xx\":\"1\"}" that is not a valid json. Apex transform (serialize) by default the @httpMethod result into a json. We could any type, in theory the result is always serialised. Mar 25, 2014 at 12:25
  • Your first sentence is wrong: you can return a graph of objects and have the platform automatically serialize them. But there are limitations to what is allowed and hopefully someone who understands those limitations will post about them. As well as meaning you have to write less code and allowing you to test using objects rather than JSON strings, this also allows the platform to deliver either JSON or XML depending on the content type requested.
    – Keith C
    Mar 25, 2014 at 13:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .