8

I need to serialize a list of SObject without omission null values. This string I need for a later usage in JavaScript. Structure is crucial, because I need to construct a table out of it. The structure is very dynamic. Different SObjects may lead to different fields and therefore different columns. Missing values will corrupt all the output and are unacceptable.

As it stands, in current versions JSON.serialize() will not output null values. There are already some questions about this raised, but either the answers there are incorrect or I'm doing something wrong to apply them right:

As far as I understand, Apex version 28.0 and newer should have no null values and version 27.0 and older should have them. But I can't make it work. There was an issue that the JSON.serialized() was not versioned right but as to @JoshKaplan answer it should be working now.

So I've created this helper-class:

public class elfApi27 { 
    public static string dump0(Object o) {
        return system.JSON.serializePretty(o); 
    }
}

Setting the version like this in the xml

<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>27.0</apiVersion>
    <status>Active</status>
</ApexClass>

tested it via execute anonymous like this (consider xl.log as system.debug)

Account a = new Account(name='test');
xt.log( elfApi27.dump0(a) );

I have tried all the versions from 32.0 down to 24.0 but the output stays rock-solid like this:

DB Callout Profile Code Escape  
32.0 APEX_CODE,WARN;APEX_PROFILING,WARN;CALLOUT,WARN;DB,WARN;SYSTEM,WARN
19:09:02.119 (119823175)|EXECUTION_STARTED
19:09:02.119 (119833395)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
19:09:02.132 (132802344)|USER_DEBUG|[230]|WARN|"{
   "attributes" : {
      "type" : "Account"
   },
   "Name" : "test"
}"
19:09:02.132 (132848199)|CODE_UNIT_FINISHED|execute_anonymous_apex
19:09:02.134 (134336841)|EXECUTION_FINISHED 

Am I misinterpreting the answers?

Is there any way to have a full verbose serialization of objects?

2 Answers 2

6

Nulls are being rendered in the latest JSON (32.0/33.0). You must explicitly the set the value to null for it to work. Consider the following code:

Account a = new Account(Name='test',Industry=null);
System.debug(LoggingLevel.ERROR, JSON.serialize(a));

Output:

{"attributes":{"type":"Account"},"Name":"test","Industry":null}

If you need every single field, you'll have to do something like:

Account a = new Account();
for(SObjectField f: account.sobjecttype.getdescribe().fields.getmap().values()) {
    try { a.put(f, null); } catch(exception e) { }
}

Even better, you could always just use a Map, which avoids the errors of field editability:

Map<String, Object> a = new Map<String, Object>();
SObject b = [SELECT Id, Name FROM Account LIMIT 1];
for(SObjectField f: account.sobjecttype.getdescribe().fields.getmap().values()) {
    try { a.put(String.valueOf(f), b.get(f)); } 
    catch(Exception e) { a.put(String.valueOf(f), null); }
}
System.debug(logginglevel.error, json.serialize(a));

I've just tried this code in executeAnonymous and came up with fully realized JSON. Note that I use a try-catch here because queried SObjects generate errors for missing fields. If you're using a manually constructed SObject, you'll not have to worry about catching errors.

Finally, note that executeAnonymous tends to always run in the latest version. You won't normally see differences in execution contexts because the entire transaction is set to version 32.0 mode when running executeAnonymous. To emulate a lower version, you'd have to write a Visualforce page and set the version of the page to a lower value, and then also set the controller's version to that lower value, etc...

Alternatively, you could write a REST class, and call that REST function. That should also set the version correctly. Since executeAnonymouse is basically an eval(), it tends to behave in the latest version, because it has to be compiled on demand.

2
  • Thx sfdcfox, as always a great answer! It will make my live a bit more complicated, since I need this in the context of a custom query capable of things SELECT *, Account.* FROM Opportunity as mentioned here salesforce.stackexchange.com/questions/49711/… - but now at least I know the backgrounds. Wouldn't it be great to have simple flag on JSON.serialize() to turn null-serialization on and of? I think there are quite good use-cases for it.
    – Uwe Heim
    Feb 12, 2015 at 19:14
  • I'm solving my special use case now by querying twice: first I fetch all the field metadata of a dynamic fieldlist, and then I lookup the values in the still sparsely populated JSON... huge rundtrip - but in the end a better result.
    – Uwe Heim
    Feb 12, 2015 at 21:15
0

I finished working on something related to this recently. I wrote a small Apex Wrapper-ish class that will grab all the writeable fields for a declared SObjectType, and compare them with the already included values in our currently instantiated records list. (My utility doesn't query for additional field values, it just makes the existing records include all missing field keys on JSON conversion). I wrote it work with any SObject.

Wrapper class at the bottom is used with a map so that we don't have to worry about field write-ability issues.

I've included all the code below. Link to repo raw: https://raw.githubusercontent.com/shingenko/SF-Apex-Portfolio/master/SObjectWrapperUtility.cls

public with sharing class SObjectWrapperUtility
{
/** Map<FieldName,IsFieldWriteable> **/
private Set<String> objectFields {get;set;}
public String jsonString;

public SObjectWrapperUtility(SObjectType objType, SObject[] items)
{

    this.objectFields = new Set<String>();
    System.debug(LoggingLevel.DEBUG, 'SObjectWrapperUtility has been initialized with objType :: ' + objType.getDescribe().getName());

    Map<String, Schema.SObjectField> fieldMap = objType.getDescribe().fields.getMap();

    for( String fieldName : fieldMap.keySet() )
    {
        Schema.SObjectField currentField = fieldMap.get(fieldName);
        this.objectFields.add(currentField.getDescribe().getName());
    }

    this.jsonString = this.generateJSON(items);
    System.debug(LoggingLevel.DEBUG, 'generated JSON string :: ' + this.jsonString);
}

private String generateJSON(SObject[] items)
{
    SObjectWrapper[] records = new SObjectWrapper[0];
    for(SObject item : items)
    {
        Map<String, Object> fields = new Map<String,Object>();
        Set<String> populatedFields = item.getPopulatedFieldsAsMap().keySet();
        System.debug(LoggingLevel.FINE, 'populatedFields output :: ' + populatedFields);

        for(String field : this.objectFields)
        {
            if(!populatedFields.contains(field))
            {
                fields.put(field,null);
            }
            else
            {
                fields.put(field, item.get(field));
            }
        }
        records.add(new SObjectWrapper((Id)item.get('Id'),fields));
    }
    return JSON.serialize(records);
}


private class SObjectWrapper
{
    public Id recordId {get;set;}
    public Map<String,Object> fields {get;set;}

    SObjectWrapper(Id recId, Map<String,Object> recFieldsMap)
    {
        this.recordId = recId;
        this.fields = recFieldsMap;
    }
}

}

Useage example:

Account[] items = [SELECT Id, Name, ContactLast3Months__c, BusinessCategory__c FROM Account LIMIT 5]; //Or however many.

String jsonString = new SObjectWrapperUtility(Account.SObjectType, items).jsonString;

System.debug(jsonString); //(This should show an output of a JSON string with all writeable and already populated fields present. Null value ones should be set accordingly.)

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.