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:
| Field | Type | Purpose |
|---|---|---|
Source_Object__c | Text(80) | API name of the source SObject |
Source_Field__c | Text(255) | API name or dot-notation path on the source |
Target_Object__c | Text(80) | API name of the target SObject |
Target_Field__c | Text(80) | API name of the target field |
Is_Active__c | Checkbox | Toggle mappings without deleting them |
Integration_Name__c | Text(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()andDescribeSObjectResult.fields.getMap(). - Catch and log: Wrap
get()andput()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
| Concern | Hard-Coded | Metadata-Driven |
|---|---|---|
| Adding a new field mapping | Code change + deploy | Metadata record + deploy (or change set) |
| Disabling a mapping | Code change + deploy | Uncheck Is_Active__c |
| Visibility into mappings | Read the code | Report on Custom Metadata records |
| Cross-environment consistency | Must deploy code | Metadata 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.