1

Use Case: In an after update trigger, I need to check a long-ish list of fields for changes, to decide whether to flag the record for re-export to an external system. I already have a wrapper object used to export the SObject (opportunity), which constructs its internal state from only the fields I care about. So the simplest way to check to check all of the fields on the records would be to construct a wrapper for the Trigger.new and Trigger.old version of each record, and compare the wrappers by value. Apex doesn't have equality-by-value comparison out of the box, but comparing the JSON-serialized strings is trivial, if the serialization order is deterministic. Note that I don't care if the serialization changes between releases; I'm only comparing within the bounds of a single Apex transaction.

I am aware that I could alternatively write a compare method for the wrappers which checks each field, but with a list of ~40 fields, I'd rather not. I could also write a method which compares the 2 SObjects using a list of field names and SObject.get(), which is probably my fallback position, but it still means maintaining both the wrapper class constructor logic AND a list of SObject fields if the list of fields changes. Comparing the wrappers by value seems to me the simplest method conceptually, but I couldn't find anything documented regarding repeatability of JSON Serialization. I've checked the Apex Docs, Apex Ref, the Canonical JSON Q&A, and searched SFSE and the web. I'm guessing there may not be an officially documented answer, but is there any reason to believe it is NOT deterministic?

Edit to add: my Apex wrapper object has only simple Apex types for properties; no SObjects or other Apex objects are contained in the wrapper.

1
  • I believe that JSON serialization of Apex objects always includes the properties in the declarative order of the properties/attributes of the Apex class and thus is repeatable for you (since they are all simple types). That said, if it ain't documented that way you shouldn't rely on it - Salesforce would be at liberty to change the way it works since JSON properties are not inherently ordered. As a slight aside, note that JSON serialization of SObjects includes the properties in the order in which they are defined or queried, so may not be repeatable depending on the use case.
    – Phil W
    Aug 7, 2021 at 17:17

2 Answers 2

2

This is not documented so you shouldn't rely on it being consistent - Salesforce would be at liberty to change the way it works since JSON properties are not inherently ordered.

As a slight aside, note that JSON serialization of SObjects seems to sort lexicographically (though in some testing I previously did the behaviour seemed different).

2
  • In my org, tested this, the values were serialized in lexicographical descending order. I think previously they were in ascending order, but I've never seen them in declarative order. But I do think you're right in saying that the order may not be deterministic.
    – sfdcfox
    Aug 7, 2021 at 17:39
  • That is weird. I checked on my org and got declaration order. Anyway, clearly this is not something that can be relied on so I will update my answer accordingly.
    – Phil W
    Aug 7, 2021 at 18:51
3

Apex doesn't have equality-by-value comparison out of the box

Yes, it does, it's called ==. This works on Maps, Sets, sObjects, and many other primitives.

I could also write a method which compares the 2 SObjects using a list of field names and SObject.get()

Or you can get all the values at once, as shown below.

but it still means maintaining both the wrapper class constructor logic AND a list of SObject fields if the list of fields changes.

You can use some sort of CMDT (Custom Metadata Type) to produce a field mapping, query from that list of fields, and avoid any code changes at all; skip a wrapper and just use a plain Map<String, Object> instead.

Here's how you can quickly determine if any fields have changed from a list of them:

Set<String> fieldsToCheck = new Set<String> {
  'Name','Industry','AnnualRevenue'
};
for(Integer i = 0, s = Trigger.size; i < s; i++) {
  Map<String, Object> oldValues = Trigger.old[i].getPopulatedFieldsAsMap().clone();
  Map<String, Object> newValues = Trigger.new[i].getPopulatedFieldsAsMap().clone();
  oldValues.keySet().retainAll(fieldsToCheck);
  newValues.keySet().retainALl(fieldsToCheck);
  if(oldValues != newValues) {
    // one or more changes detected.
  }
}

Sure, it's a few more lines of code, but JSON serialization is wicked expensive compared to comparing maps this way. You can construct your wrapper later after you figure out if you need to synchronize your data.

The fieldsToCheck could be dynamically generated from CMDT information that you could load at runtime:

Map<sObjectField, String> fieldMapping = new Map<sObjectField, String>();
for(FieldMap__mdt value: FieldMap__mdt.getAll().values()) {
  fieldMapping.put(value.Field__c, value.JSONName__c);
}

Which you can then use to both validate value changes and make serializable JSON objects using a Map.

You must log in to answer this question.

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