0

I'm strongly considering going the JSON.serialize route suggested here, but before I do, I thought I'd throw this up here. (I prefer JSONGenerator, in theory, because I can specify the field names, which JSON.serialize() isn't as flexible on.)

I'm trying to mimic report output as JSON, replicating an upload of a .csv file, and sketched out two methods; one creates an object for an individual record, the other wraps them into a larger whole.

Here's the inner method, which creates an object representing each row in the exported report - createContactDataFromIVR(): `

    String result = '';

    JSONGenerator gen = JSON.createGenerator(false);

    // basic structure = name(str), contactListId(str), data(Object), callable(boolean), phoneNumberStatus(Object)
    //  'data' being the specifics of the particular Contact ^
    gen.writeStartObject(); // '['
    gen.writeStringField('Patient IVR: Patient IVR Id', ivr.Name);
    gen.writeStringField('Phone', ivr.Phone__c);
    gen.writeStringField('Patient: First Name', ivr.First_Name__c);
    gen.writeStringField('Patient: Last Name', ivr.Last_Name__c);
    gen.writeStringField('18 Digit Contact ID', ivr.Patient__c);
    gen.writeStringField('18 Digit SFDC ID', ivr.Id);
    gen.writeStringField('Patient: Mailing Zip/Postal Code', ivr.Zip_Code__c);
    gen.writeEndObject();

    result = gen.getAsString();

    return result;

I have another method that calls this one, and attempts to add the 'contact' entry into a larger context using JSONGenerator.writeObject(object);

Running the code returns the following error:

System.JSONException: Can not write text value, expecting field name

Looking more closely at JSONGenerator example, I had preceded .writeFieldName(), .writeObject() with .writeStartObject(), which doesn't appear to be needed if you're passing in an object as a whole vs. creating it line-by-line, so I removed .writeStartObject() and went straight from .writeStringField(), the entry immediately preceding the object, to .writeFieldName().

    JSONGenerator gen = JSON.createGenerator(false);

    // basic structure = name(str), contactListId(str), data(Object), callable(boolean), phoneNumberStatus(Object)
    gen.writeStartObject(); // '['
    gen.writeStringField('name', ''); // needed(?)
    gen.writeStringField('contactListId', listId);
    gen.writeFieldName('data');

    String contactData = createContactDataFromIVR(ivr);
    gen.writeObject(contactData);
    gen.writeEndObject();

Which resulted in a new JSONException() (change is good!..?)

System.JSONException: Can not write a field name, expecting a value

Still not great -- I need that 'data' heading as that's what the consuming webservice is expecting. createContactDataFromIVR(ivr) is working fine, but I need to generate more than one, here. This is what createContactDataFromIVR() returns:

{"Patient IVR: Patient IVR Id":"ivr-07/24/18-557675","Phone":"(469) 463-0384","Patient: First Name":"J","Patient: Last Name":"V","18 Digit Contact ID":"0033300001f4XqnAAE","18 Digit SFDC ID":"a19e00000034RxTAAU","Patient: Mailing Zip/Postal Code":"75048"}

The examples that I've found (JSONGenerator Sample) indicate that an Object can be inserted with .writeFieldName('someName'), followed by .writeObject('theObject'). I've used JSONGenerator on 'theObject' already; perhaps that's where it's breaking. I'll try JSON.serialize() on it instead (though as I mentioned at the top^, I'm trying to mimic a .csv exported report, and I can't emulate the field names as precisely using .serialize()..)

0

You could use JSON.serialize, just not in the way you're thinking. All you need is a Map:

Map<String, Object> output = new Map<String, Object>();
List<Map<String, Object>> data = new List<Map<String, Object>>();
for(IVR__C record: ivrList) {
  data.add(createContactDataFromIVR(record));
}
output.put('data', data);
String jsonResult = JSON.serialize(output);

The actual method I mention above is really just creating a map:

public Map<String, Object> createContactDataFromIVR(IVR__c record) {
  return new Map<String, Object> {
    'Patient IVR: Patient IVR Id' => ivr.Name,
    'Phone' => ivr.Phone__c,
    'Patient: First Name' => ivr.First_Name__c,
    'Patient: Last Name' => ivr.Last_Name__c,
    '18 Digit Contact Id' => ivr.Patient__c,
    '18 Digit SFDC Id' => ivr.Id,
    'Patient: Mailing Zip/Postal Code' => ivr.Zip_Code__c
  };
}

By using simple primitives, you don't need to write custom wrapper classes, and you avoid the mess of trying to get JSONGenerator to work, which is really only meant to be available for porting Java code that uses it. It's an incredibly inefficient library compared to the other methods of JSON generation.

  • Very clean! I haven't come across that particular syntax for creating a map (and maybe it works with some additional '{}' around the lines?) - I ended up going with the old-fashioned Map<String, Object> thisMap = new Map<String, Object>();, and then added each line separately using thisMap.put('Phone', ivr.Phone__c), for instance. I do need additional values outside this list, which did result in a different order when I used JSON.serialize() vs. JSONGenerator, but it shouldn't make a difference..? I'll find out :D – Duncan Stewart Jul 27 '18 at 17:33
  • @DuncanStewart Actually, it was mistyped. I wasn't paying attention. You use the key => value syntax. See my edit. And yes, the order will probably be different; JSONGenerator explicitly allows you to order the values, but it shouldn't matter; any proper JSON parser shouldn't care about order. – sfdcfox Jul 27 '18 at 17:35
  • Nice. :D @sfdcfox – Duncan Stewart Jul 27 '18 at 17:45
0

I'm adding this answer so that you can see the final code, based on sfdcfox's answer (I really do need to learn how to create user references..). I'm leaving my previous answer there as well, as that does retain the exact structure of the JSON I'm trying to create - I'll see whether that's critical or not soon...

Anyhow, here are the two methods I ended up with:

public static Map<String, Object> createContactDataFromIVR(Patient_IVR__c ivr){

    Map<String, Object> lineItemMap = new Map<String, Object> ();
    lineItemMap.put('Patient IVR: Patient IVR Id', ivr.Name);
    lineItemMap.put('Phone', ivr.Phone__c);
    lineItemMap.put('Patient: First Name', ivr.First_Name__c);
    lineItemMap.put('Patient: Last Name', ivr.Last_Name__c);
    lineItemMap.put('18 Digit Contact ID', ivr.Patient__c);
    lineItemMap.put('18 Digit SFDC ID', ivr.Id);
    lineItemMap.put('Patient: Mailing Zip/Postal Code', ivr.Zip_Code__c);

    return lineItemMap;
}

public static String createContactListUpdate(String listId, Patient_IVR__c[] ivrs){

    String result = '';

    Map<String, Object> finalMap = new Map<String, Object> ();

    finalMap.put('name', '');
    finalMap.put('contactListId', listId);
    String data = '';        
    for(Patient_IVR__c ivr : ivrs){
        data += createContactDataFromIVR(ivr);
    }
    finalMap.put('data', data);
    finalMap.put('callable', true);
    finalMap.put('phoneNumberStatus', '');

    result = JSON.serialize(finalMap);

    return result;
}

which results in this output (with a single Patient_IVR__c entry passed into it)

{"phoneNumberStatus":"","callable":true,"data":"{18 Digit Contact ID=0033300001f4XqnAAE, 18 Digit SFDC ID=a19e00000034RxTAAU, Patient IVR: Patient IVR Id=ivr-07/24/18-557675, Patient: First Name=J, Patient: Last Name=V, Patient: Mailing Zip/Postal Code=75048, Phone=(469) 463-0384}","contactListId":"listId","name":""}

0

Here's where trying to break down my code into the smallest components possible, for more(?) simplified testing, broke down - separating out the item-level object-to-JSON code into its own method, and then trying to wrap that object in another JSONGenerator method failed.

This works, though - using a standard Apex loop to iterate over the individual records, as part of a single JSONGenerator method:

    gen.writeStringField('contactListId', listId);
    gen.writeFieldName('data');
    for(Patient_IVR__c ivr : ivrs){
        gen.writeStartObject();
            gen.writeStringField('Patient IVR: Patient IVR Id', ivr.Name);
            gen.writeStringField('Phone', ivr.Phone__c);
            gen.writeStringField('Patient: First Name', ivr.First_Name__c);
            gen.writeStringField('Patient: Last Name', ivr.Last_Name__c);
            gen.writeStringField('18 Digit Contact ID', ivr.Patient__c);
            gen.writeStringField('18 Digit SFDC ID', ivr.Id);
            gen.writeStringField('Patient: Mailing Zip/Postal Code', ivr.Zip_Code__c);
        gen.writeEndObject();
    }

And the output: {"name":"","contactListId":"listId","data":{"Patient IVR: Patient IVR Id":"ivr-07/24/18-557675","Phone":"(469) 463-0384","Patient: First Name":"J","Patient: Last Name":"V","18 Digit Contact ID":"0033300001f4XqnAAE","18 Digit SFDC ID":"a19e00000034RxTAAU","Patient: Mailing Zip/Postal Code":"75048"},"callable":true,"phoneNumberStatus":{}}

  • Note: I added this ^ before seeing our good friend sfdcfox's response, which (as expected) looks very clean indeed. – Duncan Stewart Jul 27 '18 at 16:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.