Skip to main content

Metadata-Driven Field Mapping for Flexible Salesforce Integrations

Hard-coding field mappings in Apex classes creates a maintenance burden. Every new field requires a code change, a test class update, and a deployment. For integration-heavy orgs with dozens of mapped fields across multiple objects, this approach does not scale.

A better pattern uses Custom Metadata Types to define field mappings declaratively. Admins can add, modify, or disable mappings without touching code.

Designing the Custom Metadata Type

Create a Custom Metadata Type called Field_Mapping__mdt with these fields:

FieldTypePurpose
Source_Object__cText(80)API name of the source SObject
Source_Field__cText(255)API name or dot-notation path on the source
Target_Object__cText(80)API name of the target SObject
Target_Field__cText(80)API name of the target field
Is_Active__cCheckboxToggle mappings without deleting them
Integration_Name__cText(80)Groups mappings by integration (e.g., "INT06", "INT11")

The Source_Field__c supports dot-notation for relationship traversal (e.g., Quote.Pricebook2.Name), enabling mappings from deeply nested related objects.

Building Dynamic SOQL from Metadata

Query the active mappings for a given integration and construct a SOQL query that includes all required source fields:

public class DynamicFieldMapper {

public static List<Field_Mapping__mdt> getMappings(String integrationName) {
return [
SELECT Source_Object__c, Source_Field__c,
Target_Object__c, Target_Field__c
FROM Field_Mapping__mdt
WHERE Integration_Name__c = :integrationName
AND Is_Active__c = true
];
}

public static String buildQuery(
String sourceObject,
List<Field_Mapping__mdt> mappings,
String whereClause
) {
Set<String> fields = new Set<String>{ 'Id' };
for (Field_Mapping__mdt mapping : mappings) {
if (mapping.Source_Object__c == sourceObject) {
fields.add(mapping.Source_Field__c);
}
}

return 'SELECT ' + String.join(new List<String>(fields), ', ')
+ ' FROM ' + sourceObject
+ ' WHERE ' + whereClause;
}
}

Traversing Dot-Notation Field Paths

The core challenge is reading values from relationship paths like Quote.Pricebook2.Name. Salesforce stores related object data as nested SObject instances accessible via getSObject() and get():

public static Object getFieldValue(SObject record, String fieldPath) {
List<String> parts = fieldPath.split('\\.');

// Traverse relationship chain up to the final field
SObject current = record;
for (Integer i = 0; i < parts.size() - 1; i++) {
if (current == null) {
return null;
}
current = current.getSObject(parts[i]);
}

if (current == null) {
return null;
}

// Read the terminal field value
return current.get(parts[parts.size() - 1]);
}

Applying Values to Target Records

Combine the mapping metadata with the traversal logic to populate target records:

public static SObject applyMappings(
SObject sourceRecord,
String targetObjectType,
List<Field_Mapping__mdt> mappings
) {
Schema.SObjectType targetType = Schema.getGlobalDescribe().get(targetObjectType);
SObject target = targetType.newSObject();

for (Field_Mapping__mdt mapping : mappings) {
try {
Object value = getFieldValue(sourceRecord, mapping.Source_Field__c);
if (value != null) {
target.put(mapping.Target_Field__c, value);
}
} catch (Exception e) {
System.debug(LoggingLevel.WARN,
'Mapping failed for ' + mapping.Source_Field__c
+ ' -> ' + mapping.Target_Field__c + ': ' + e.getMessage());
}
}

return target;
}

Error Handling

Invalid field names in metadata records will throw SObjectException at runtime. Defensive strategies include:

  • Validate on save: Use a before-insert trigger on the Custom Metadata Type to verify field API names against Schema.getGlobalDescribe() and DescribeSObjectResult.fields.getMap().
  • Catch and log: Wrap get() and put() calls in try-catch blocks (as shown above) so one bad mapping does not abort the entire batch.
  • Dry-run mode: Provide an admin utility that tests all active mappings against sample records and reports errors before they hit production data.

Maintenance Benefits

ConcernHard-CodedMetadata-Driven
Adding a new field mappingCode change + deployMetadata record + deploy (or change set)
Disabling a mappingCode change + deployUncheck Is_Active__c
Visibility into mappingsRead the codeReport on Custom Metadata records
Cross-environment consistencyMust deploy codeMetadata travels with change sets or packages

This pattern is particularly valuable in orgs with multiple integration points. Each integration gets its own set of metadata records identified by Integration_Name__c, and the same Apex engine processes all of them.