JSON Field Storage Pattern
Problem Context
In Salesforce development, we often encounter scenarios where we need to store structured, supplementary data that:
Is primarily for display or informational purposes
Won't be used in SOQL queries, workflows, or process automation
Varies in structure between records
Would otherwise require creating numerous custom fields or related objects
Needs to maintain flexibility for future changes without schema modifications
Traditional approaches like creating custom objects or fields for every piece of data can lead to:
Object proliferation and complexity
Hitting Salesforce limits (custom field limits, object limits)
Maintenance overhead for rarely-used fields
Performance impacts from excessive joins
Schema rigidity that hampers rapid iteration
Solution: JSON Storage in LongTextArea Fields
The pattern leverages Salesforce's LongTextArea
fields to store JSON-serialized data, providing a flexible, schema-less approach for non-transactional information.
Field Definition Pattern
Define a generic Metadata__c
field on objects that need flexible data storage:
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Metadata__c</fullName>
<description>Stores supplementary data as JSON for display purposes</description>
<externalId>false</externalId>
<label>Metadata</label>
<length>131072</length>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
<type>LongTextArea</type>
<visibleLines>3</visibleLines>
</CustomField>
Key Configuration Decisions:
Field Type:
LongTextArea
provides up to 131,072 characters (sufficient for complex JSON structures)History Tracking: Generally disabled for performance (JSON diffs are not user-friendly)
Field Naming: Use consistent naming like
Metadata__c
or domain-specific names likeReviewData__c
Implementation Patterns
1. Configuration Storage Pattern
Store variable configuration mappings that drive application behavior:
public class DamageQuestionnaireConfigParser {
public Map<String, Id> parse(TortConfiguration__c record) {
try {
// Parse JSON configuration mapping damage types to questionnaire IDs
return (Map<String, Id>) JSON.deserialize(
record.Metadata__c,
Map<String, Id>.class
);
} catch (Exception e) {
Logger.error('TortConfiguration__c.Metadata__c parsing failed.', record, e);
return null;
}
}
}
Example JSON Structure:
{
"Real Property": "a0X1234567890ABC",
"Personal Property": "a0X1234567890DEF",
"Business Loss": "a0X1234567890GHI"
}
2. Dynamic Options Storage Pattern
Store dynamic form options that vary by record:
public class QuestionOptionParser {
public List<QuestionOption> parse(DamageQuestion__c record) {
if (!isSelectType(record)) return null;
try {
return (List<QuestionOption>) JSON.deserialize(
record.Options__c,
List<QuestionOption>.class
);
} catch (Exception e) {
Logger.error('DamageQuestion__c.Options__c parsing failed.', record, e);
return new List<QuestionOption>();
}
}
}
Example JSON Structure:
[
{"label": "Single Family Home", "value": "single_family"},
{"label": "Multi-Family", "value": "multi_family"},
{"label": "Commercial", "value": "commercial"}
]
3. Document Metadata Pattern
Store supplementary document information from external systems:
public class FileUploadController {
@AuraEnabled
public static String createDocument(DocumentInfo documentInfo) {
Document__c document = new Document__c(
Case__c = documentInfo.caseId,
FileName__c = documentInfo.fileName,
FilePath__c = documentInfo.filePath,
Metadata__c = documentInfo.metadata, // JSON from external system
Type__c = documentInfo.type
);
// Use Unit of Work pattern for transaction management
fflib_ISObjectUnitOfWork unitOfWork = Application.UnitOfWork.newInstance(
new List<SObjectType>{ Document__c.SObjectType }
);
unitOfWork.registerNew(document);
unitOfWork.commitWork();
return document.Id;
}
}
Display Pattern in Lightning Web Components
Parsing and Flattening JSON for Display
The metadataView
LWC demonstrates how to parse and display JSON data:
export default class MetadataView extends LightningElement {
metadata = [];
handleLightningEvent(payload) {
try {
const parsedMetadata = JSON.parse(payload.body.metadata);
this.metadata = this.flattenMetadata(parsedMetadata);
} catch (error) {
this.metadata = [];
}
}
flattenMetadata(metadata) {
// Recursive flattening for nested JSON structures
function flattenObject(objectValue) {
return Object.keys(objectValue).reduce((result, key) => {
const value = objectValue[key];
const defaultValue = { label: key, value };
// Handle nested objects/arrays recursively
if (typeof value === 'object' && value !== null) {
return result.concat(flattenObject(value));
}
return result.concat(defaultValue);
}, []);
}
return flattenObject(metadata);
}
}
Display Template Pattern
<template lwc:if={metadata.length}>
<lightning-layout multiple-rows>
<template for:each={metadata} for:item="field">
<lightning-layout-item key={field.label} padding="horizontal-small" size="6">
<div class="slds-form-element">
<span class="slds-form-element__label">{field.label}</span>
<div class="slds-form-element__static">{field.value}</div>
</div>
</lightning-layout-item>
</template>
</lightning-layout>
</template>
Benefits of This Approach
1. Schema Flexibility
Add new data attributes without deploying metadata changes
Adapt to changing requirements without modifying object schema
Support varying data structures across records of the same type
2. Performance Optimization
Reduce custom field count (helps with query selectivity)
Minimize object relationships for non-queryable data
Single field retrieval for all supplementary data
3. Development Velocity
Rapid iteration on data structures
No deployment dependencies for data model changes
Easier integration with external systems that provide JSON
4. Maintenance Simplicity
Centralized storage pattern
Consistent parsing approach
Clear separation between transactional and display data
When to Use This Pattern
Ideal Use Cases
External System Data: Information from APIs that won't be queried
Configuration Storage: Variable settings that differ by context
Document Metadata: File properties, processing results, audit information
Form Responses: Dynamic questionnaire answers, survey data
Integration Payloads: Preserving original data from external sources
Audit/History Data: Snapshots of record states for reference
When NOT to Use This Pattern
Queryable Data: Any field that needs SOQL filtering
Workflow Criteria: Data used in automation rules
Reporting Fields: Information needed in reports/dashboards
Frequently Updated Data: High-volume transactional updates
Regulated Data: Information requiring field-level security
Best Practices and Considerations
1. Error Handling
Always implement robust error handling for JSON parsing:
try {
return JSON.deserialize(jsonString, TargetType.class);
} catch (Exception e) {
// Log error with context
Logger.error('JSON parsing failed for record', record, e);
// Return safe default
return getDefaultValue();
}
2. Validation
Consider implementing JSON schema validation for critical data:
public Boolean isValidMetadata(String jsonString) {
try {
Map<String, Object> parsed = (Map<String, Object>) JSON.deserializeUntyped(jsonString);
// Validate required keys exist
return parsed.containsKey('requiredField1')
&& parsed.containsKey('requiredField2');
} catch (Exception e) {
return false;
}
}
3. Size Management
Monitor field usage to avoid hitting the 131KB limit:
public void saveMetadata(String jsonData) {
if (jsonData.length() > 130000) {
throw new MetadataException('Metadata exceeds size limit');
}
// Proceed with save
}
4. Documentation
Always document the expected JSON structure:
/**
* Metadata__c field structure:
* {
* "source": "ExternalSystem",
* "processedDate": "2024-01-15",
* "attributes": {
* "category": "TypeA",
* "priority": "High"
* }
* }
*/
5. Type Safety
Create wrapper classes for complex structures:
public class DocumentMetadata {
public String source;
public DateTime processedDate;
public Map<String, String> attributes;
public static DocumentMetadata parse(String jsonString) {
return (DocumentMetadata) JSON.deserialize(
jsonString,
DocumentMetadata.class
);
}
}
Common Pitfalls to Avoid
Using JSON fields for queryable data - This defeats the purpose and creates maintenance nightmares
Storing sensitive data - JSON fields bypass field-level security
Neglecting error handling - Invalid JSON will break your application
Over-nesting structures - Keep JSON reasonably flat for maintainability
Missing data migration strategy - Plan for structure evolution
Ignoring governor limits - Large JSON processing can hit CPU limits
Conclusion
The JSON field storage pattern provides a powerful solution for managing non-transactional, display-oriented data in Salesforce. By understanding when and how to apply this pattern, development teams can build more flexible, maintainable applications while avoiding the overhead of excessive custom fields and objects.
Remember: This pattern complements, not replaces, proper data modeling. Use it judiciously for the right use cases to maximize its benefits while maintaining system integrity and performance.
Last updated
Was this helpful?