I have a Salesforce Apex REST service (LeadService) that processes incoming leads in JSON format.Additionally, I'm looking for ways to enhance error handling and make the code more maintainable.Provide suggestions or improvements for optimizing the bulk upsert process and enhancing the overall code structure
@RestResource(urlMapping='/api/lead')
global class Service {
@HttpPost
global static string createdata(){
RestResponse res = Restcontext.response;
String requestBody = RestContext.request.requestBody.toString();
try {
// Deserialize JSON data into a list of LeadDataDeserializer objects
List<LeadDataDeserializer> externalLeads = (List<LeadDataDeserializer>)
JSON.deserializeStrict(requestBody, List<LeadDataDeserializer>.class);
// Transform LeadDataDeserializer objects into Lead data
List<Lead> students = new List<Lead>();
for(LeadDataDeserializer info : externalLeads) {
Lead t_leads = setLeadFields(info);
t_leads.company='Test Company';
students.add(t_leads);
}
if(students.isEmpty()){
res.statusCode=400;
return 'Empty list';
}
else{
List<Response> responseretn=new List<Response>();
Database.UpsertResult[] srList = Database.upsert(students, Lead.External_Id__c, false);
// Process upsert results if needed
Integer i=0;
for(Database.UpsertResult upResult:srList){
if(upResult.isSuccess()){
responseretn.add(new Response(upResult.getId(),true,students[i].MobilePhone));
}
else{
System.debug(upResult.getErrors());
responseretn.add(new Response(upResult.getId(),false,students[i].MobilePhone));
}
i+=1;
}
String jsonReqBody=JSON.serialize(responseretn);
res.statusCode = 201;
return jsonReqBody;
}
} catch(Exception e) {
// Handle exceptions
res.Statuscode = 500;
return 'Internal Server Error';
}
}
//Response wrapper to return
public class Response{
public string leadId{get;set;}
public boolean isSuccess{get;set;}
public string mobilePhone{get;set;}
public Response(String leadId,Boolean isSuccess,String mobilePhone){
this.leadId=leadId;
this.isSuccess=isSuccess;
this.mobilePhone=mobilePhone;
}
}
public static Lead setLeadFields(LeadDataDeserializer info){
Lead extLead=new Lead();
extLead.LastName=info.Name;
extLead.CountryCode__c=info.countryCode;
extLead.MobilePhone=info.phoneNumber;
extLead.mx_WhatsApp_Number__c=extLead.MobilePhone;
extLead.mx_IP_Address__c=info.ipAddress;
extLead.External_Id__c=extLead.MobilePhone;
if(info.leadStage!=null){
extLead.Status=info.leadStage;
}
if(info.campaignName!=null){
extLead.mx_Campaign_Name__c=info.campaignName;
}
if(info.campaignSource!=null){
extLead.SourceCampaign__c=info.campaignSource;
}
//20 more if conditions with null check like above (serializeddata.field!=null)
return extLead;
}}
I'm using multiple if statements to handle only to include fields which have value and not to overide the previous record value if the value came from integration is empty
How to can this be handled efficiently ?
if(students.isEmpty()){
res.statusCode=400;
return 'Empty list';
}
this is bit late. Won't change much in performance but logically it'd be nicer to check the deserialized list earlier and return then, not after making leads out of it.
I don't think you have to return a string. You could easily return List<Response>
and SF will serialize it for you.
How do you want to handle problems? Save what you can? You could insert helper sObject for any problems and run a report on that. Or use platform events with "publish immediately" so some monitoring system or even special apex trigger could process them.
I'd maybe include counter of errors so if it's 100% fail rate return something else than 201
What if you do if it sends > 10K rows (or the side effects will result in > 10K dmls)... If it's a legit concern I'd probably rewrite it to kicking off a batch job (batches can take scope and iterate over it, they don't always have to start with a query). Bonus points that you could implements Database.RaisesPlatformEvents
and job almost done, SF will do a lot of error handling for you without need for manual "savepoint-try-catch-rollback-insert task or whatever"
As for actual mapping code... it's not great but not very bad either. You may have it bit too naive with null checks - for some variables String.isNotBlank
will be better, depending on what JSON is produced by the source.
I'd keep the source-target field mapping somewhere else (custom setting? custom metadata?) so you don't have to recompile it, deploy etc every time you add new field.
If you really feel fancy you could read up about JSON.serialize with the parameter to skip nulls (so for example you could deserialize input, serialize it back with nulls skipped, deserialize that 2nd time, clean). Or blindly set all fields based on the input, then sObject.getPopulatedFieldsAsMap and iterate through them checking what's null... but it feels like bit too clever for what's needed. Sometimes simple is most effective.
Does it even have to be a custom REST API? If you have control over the source format you could consider composite
over standard api, less work. Have a look at my https://salesforce.stackexchange.com/a/274696/799 (including the "allOrNone" header). Normal upsert doesn't handle multiple records well, true - but this is close enough!
And well... Code isn't always the answer. If you're absolutely sure standard API won't cut it - did you know you can call flows over REST api?
====
Edit
Here's a good sample (you can run it in "Execute anonymous"
public class Wrapper {
public Boolean isActive;
public String name;
public String email;
public String phone;
}
String text = '['+
' {'+
// ' \"isActive\": false,'+
' \"name\": \"Blankenship Ryan\",'+
' \"email\": \"blankenshipryan@naxdis.com\",'+
' \"phone\": \"+1 (803) 465-3324\"'+
' },'+
' {'+
' \"isActive\": false,'+
// ' \"name\": \"Herring Blevins\",'+
' \"email\": \"herringblevins@naxdis.com\",'+
' \"phone\": \"+1 (938) 592-2521\"'+
' },'+
' {'+
' \"isActive\": true,'+
' \"name\": \"Paige Holman\",'+
// ' \"email\": \"paigeholman@naxdis.com\",'+
' \"phone\": \"+1 (968) 576-3874\"'+
' },'+
' {'+
' \"isActive\": false,'+
' \"name\": \"Meadows Clemons\",'+
' \"email\": \"meadowsclemons@naxdis.com\"'+ // removed comma here
// ' \"phone\": \"+1 (806) 463-3276\"'+
' },'+
' {'+
' \"isActive\": true,'+
' \"name\": \"Shawna Holt\",'+
' \"email\": \"shawnaholt@naxdis.com\",'+
' \"phone\": \"+1 (956) 542-2138\"'+
' },'+
' {'+
// ' \"isActive\": false,'+
// ' \"name\": \"Tonya Britt\",'+
// ' \"email\": \"tonyabritt@naxdis.com\",'+
// ' \"phone\": \"+1 (845) 543-2721\"'+
' }'+
']';
System.debug(text);
List<Wrapper> wrappers = (List<Wrapper>) JSON.deserializeStrict(text, List<Wrapper>.class);
System.debug('nulls should show');
System.debug(wrappers);
List<Wrapper> wrappers2 = new List<Wrapper>();
for(Wrapper w : wrappers){
String stripped = JSON.serialize(w, true);
System.debug('should have no nulls: ' + stripped);
Wrapper w2 = (Wrapper) JSON.deserialize(stripped, Wrapper.class);
System.debug('but deserialized - they pop back: ' + w2);
}
It's the deserializing that's the problem, even if your source system doesn't send nulls.
There are some tricks in https://salesforce.stackexchange.com/questions/257158/json-serialize-is-it-possible-to-suppress-null-values-of-a-map you could play with or loop through the lead's getPopulatedFieldsAsMap and copy them over to final lead being upserted only if they aren't null... But that sounds bit clever, nothing wrong with your nice readable list of ifs. Yes, it's boring, maybe could be read from some config... but it works.