[SalesForce] JSONGenerator writeObjectField() remove backslashes

I'm trying to create an Invocable Method which allows an input of a JSON-formatted string from Process Builder as a variable. However, when I try to add the inputted JSON string using JSONGenerator.writeObjectField(), it escapes the double quotes with a backslash. This is causing the intended endpoint, Zapier, to incorrectly parse "Data" as one giant string instead of a nested object.

I've tried String.replace() and String.remove(), neither worked. Should I just ditch JSONGenerator altogether and manually construct the JSON?

Anonymous Apex:

ZapierWebhook_Invocable.RequestDetail rd = new ZapierWebhook_Invocable.RequestDetail();
rd.webhookURL = 'http://requestbin.fullcontact.com/[endpoint]';
rd.customJSON = '{"AssigneeId" : "B123456", "PermissionSetId" : "A789101112"}';
ZapierWebhook_Invocable.processRequests(new List<ZapierWebhook_Invocable.RequestDetail>{rd});

Relevant class method:

EDIT: To clarify, the Strings in bodyArray are JSON-formatted objects as strings (all Process Builder input values in the transaction), stored in the ZapierWebhook_Invocable.RequestDetail.customJSON invocable variable.

private static void sendRequest(String endpoint, List<String> bodyArray) {

    HttpRequest req = new HttpRequest();
    req.setEndpoint(endpoint);
    req.setMethod('POST');
    req.setHeader('Content-Type','application/json;charset=UTF-8');

    JSONGenerator gen = JSON.createGenerator(false);
    gen.writeStartObject();
    gen.writeStringField('Instance_URL',Url.getSalesforceBaseUrl().toExternalForm());
    gen.writeObjectField('Data',bodyArray);
    gen.writeEndObject();

    req.setBody(gen.getAsString());

    Http http = new Http();
    http.send(req);
}

Raw Body:

{"Instance_URL":"https://myDomain.my.salesforce.com","Data":["{\"AssigneeId\" : \"B123456\", \"PermissionSetId\" : \"A789101112\"}"]}

Zapier's debug:

enter image description here

UPDATE: The following workaround works as expected (assigning the return value as the request body). Not ideal…still would like to know how to get JSONGenerator to work.

private static String getBodyJSON(List<String> bodyArray) {

    String instanceURL = Url.getSalesforceBaseUrl().toExternalForm();
    String runningUserName = UserInfo.getName();

    List<String> jsonObjectList = new List<String>();
    String body;
    for(String jsonObj : bodyArray) {
        body = '{';
        body = body + '"Instance_URL": "' + instanceURL + '", ';
        body = body + '"Running_User": "' + runningUserName + '", ';
        body = body + '"Data": ' + jsonObj;
        body = body + '}';
        jsonObjectList.add(body);
    }

    return '[' + String.join(jsonObjectList,',') + ']';
}

Best Answer

JSONGenerator is hard to use because you have to exactly embody the JSON structure in the code e.g. include writeStartArray() calls.

For just about all cases, the JSON.serialize method is much easier to code because it works out the serialization based on the data passed to it:

String s = JSON.serialize(new Map<String, Object>{
    'Instance_URL' => Url.getSalesforceBaseUrl().toExternalForm(),
    'Running_User' => UserInfo.getName(),
    'Data' => bodyArray
});
req.setBody(s);

and as illustrated here works with collections such as maps and lists we well as on properties of classes. (I'm not sure what structure you are looking for: create nested maps and lists as required and then serialize that object graph.)

Note that building JSON by concatenating strings is risky as things like embedded double quotes are not escaped, meaning the code may work most of the time but will break in some data-dependent cases.

PS

If the List<String> bodyArray argument is in fact an array of JSON strings already, the best option is to change the calling code to pass the structured data instead. If you can't do that, then deserializing first should avoid the escaping:

    'Data' => JSON.deserializeUntyped(bodyArray)