Skip to content

DTOs - Guide

Framework: KernDX Package Type: Managed Package (namespace-agnostic) Version: 1.0 Last Updated: April 2026

> Note for Subscriber Orgs: When using KernDX as a managed package, prefix framework class references with your installed namespace (e.g., AcmeLib.DTO_JsonBase for Acme > Inc.).


Table of Contents

Expand
  1. Quick Navigation
  2. Overview
  3. Architecture
  4. Quick Start
  5. When to Use DTOs
  6. Working with Base DTO Classes
  7. Built-in DTO Classes
  8. Creating Custom DTO Classes
  9. Advanced DTO Patterns
  10. Integration Patterns
  11. Testing
  12. Anti-Patterns
  13. Best Practices
  14. Troubleshooting
  15. Reference
  16. Related Documentation

Quick Navigation

I am a...I need to...Go to...
ArchitectUnderstand DTO architectureArchitecture
ArchitectChoose integration patternsIntegration Patterns
DeveloperCreate my first DTOQuick Start
DeveloperBuild custom DTO classesCreating Custom DTO Classes
DeveloperImplement advanced patternsAdvanced DTO Patterns
AnalystKnow when to use DTOsWhen to Use DTOs

Overview

Data Transfer Objects (DTOs) are lightweight objects designed to transfer data between different layers of your application. DTOs provide a structured, type-safe way to serialize and deserialize data for:

  • Web Service Integration - REST request and response payloads
  • Lightning Web Components - Structured data from Apex to LWC
  • Flow Integration - Complex data structures in invocable methods
  • Data Transformation - Converting between SObjects and external formats
  • Testing - Creating consistent test data structures

> DTO Framework Scope: a set of base classes plus built-in DTOs (JSON, name-value, datatable, picklist, and CDC change-event header), supporting JSON serialization, SObject transformation, JsonPath > navigation, and sorted DTO collections.

> Responsibilities: DTOs transport data between layers (Apex to LWC, Apex to external APIs, Flow to Apex). They do not contain business > logic, perform DML, or query data. Population logic in populate() should be limited to mapping fields from selectors and parameters.

Key Benefits

  • Separation of Concerns - Decouple external data formats from internal SObject structure
  • Type Safety - Strongly-typed data structures prevent runtime errors
  • Versioning - Maintain API contracts without changing SObjects
  • Flexibility - Transform data between different representations
  • Testability - Easy to mock and verify in unit tests

DTO Framework Components

The KernDX framework provides:

  1. Base Classes - DTO_Base, DTO_JsonBase
  2. Specialized DTOs - DTO_NameValues, DTO_BaseTable, DTO_PickList
  3. Utility Integration - JsonPath, comparators, serialization helpers

Architecture

The DTO framework follows a layered inheritance model where all DTOs derive from a common abstract base class. This enables consistent serialization, deserialization, equality, and population behaviour across JSON formats. The framework sits between your business logic and external consumers (REST APIs, LWC, Flows), providing a clean separation between internal SObject structures and external data contracts.

The key architectural components are:

  • DTO_Base - Abstract foundation providing serialize(), deserialize(), populate(), transform(), equals(), and hashCode()
  • DTO_JsonBase - JSON-specific implementation with pretty-print serialization, JsonPath integration, and FieldComparator sorting
  • Built-in DTOs - DTO_NameValues (key-value parameters), DTO_BaseTable (Lightning datatable), DTO_PickList (picklist metadata), DTO_ChangeEventHeader (CDC change-event header)

DTO Class Hierarchy

text
DTO_Base (Abstract base class)
+-- DTO_JsonBase (JSON serialization)
    +-- DTO_NameValues (Key-value collections)
    +-- DTO_BaseTable (Lightning datatable structure)
    +-- [Your custom JSON DTOs]

Standalone DTOs (no inheritance):
+-- DTO_NameValue (Single name-value pair)
+-- DTO_PickList (Picklist metadata)
+-- DTO_PicklistValue (Single picklist value)

Quick Start

The most common DTO pattern is creating a JSON DTO for an API integration or LWC response. Here is the simplest path from zero to working DTO:

apex
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_OrderSummary extends DTO_JsonBase
{
	@AuraEnabled public String orderId;
	@AuraEnabled public Decimal totalAmount;
}

// Create and serialize
DTO_OrderSummary summary = new DTO_OrderSummary();
summary.orderId = 'ORD-001';
summary.totalAmount = 250.00;
String json = summary.serialize();

// Deserialize from JSON
DTO_OrderSummary parsed = (DTO_OrderSummary)new DTO_OrderSummary().deserialize(json);

> Subscriber Orgs: Always include @JsonAccess(Serializable='always' Deserializable='always') on every DTO that extends a managed package base class — without it, > serialization fails at runtime. See Type Resolution for additional requirements.

For deeper coverage, continue reading the sections below.


When to Use DTOs

Use DTOs When:

Building REST APIs - Standardize request/response formats

apex
// Inbound API with DTO
public with sharing class API_CreateAccount extends API_Inbound
{
	public override void configure()
	{
		super.configure();
		requestPayload = new DTO_AccountRequest();
		responsePayload = new DTO_AccountResponse();
	}
}

Integrating External Systems - Parse REST responses

apex
// Parse external API response
DTO_WeatherResponse weather = (DTO_WeatherResponse)
	new DTO_WeatherResponse().deserialize(responseBody);

Returning Complex Data to LWC - Structure hierarchical data

apex
@AuraEnabled
public static DTO_BaseTable getAccountData()
{
	DTO_BaseTable table = new DTO_BaseTable();
	// Build table structure...
	return table;
}

Flow Invocable Methods - Pass complex parameters

apex
@InvocableMethod
public static List<Results> processData(List<Requests> requests)
{
	// Use DTO_NameValues for flexible parameters
}

Don't Use DTOs When:

Working Within Apex - Use SObjects directly

apex
// BAD: Unnecessary DTO usage
DTO_Account dtoAccount = convertToDto(account);
processAccount(dtoAccount);

// GOOD: Direct SObject usage
processAccount(account);

Simple Data Types - Use primitives or simple maps

apex
// BAD: Over-engineering
DTO_StringValue dto = new DTO_StringValue();
dto.value = 'Hello';

// GOOD: Use String directly
String value = 'Hello';

Internal Database Operations - Use SObjects with DML framework

apex
// BAD: Converting to DTO for DML
DTO_Account dtoAccount = mapToDto(account);
DML_Builder.newTransaction().doInsert(convertToSObject(dtoAccount)).execute();

// GOOD: Direct SObject DML
DML_Builder.newTransaction().doInsert(account).execute();

Working with Base DTO Classes

DTO_Base - Foundation

DTO_Base is the abstract foundation for all DTOs, providing core functionality:

apex
global abstract class DTO_Base
{
	global virtual String serialize()
	global virtual DTO_Base deserialize(String dtoString)
	global virtual void populate(Id recordId)
	global virtual void populate(Id recordId, DTO_NameValues dtoRequestParameters)
	global virtual void transform(DTO_Base dtoBase)
	public Boolean equals(Object obj)
	public Integer hashCode()
}

Key Methods:

MethodPurposeOverride Required
serialize()Convert DTO to string formatYes (in subclasses)
deserialize()Parse string to DTOYes (in subclasses)
populate(Id)Load DTO from record IDOptional
transform()Convert between DTO typesOptional
equals() / hashCode()Support collectionsNo (implemented)

Example: Using equals() in Collections

apex
Set<DTO_Base> uniqueDtos = new Set<DTO_Base>();
uniqueDtos.add(dto1);
uniqueDtos.add(dto2);
uniqueDtos.add(dto1); // Duplicate ignored

if(dto1.equals(dto2))
{
	// DTOs are equal based on serialized content
}

DTO_JsonBase - JSON Handling

DTO_JsonBase extends DTO_Base for JSON serialization with advanced features:

Key Features:

  1. Automatic JSON Serialization - Pretty-print with null suppression
  2. Type-Safe Deserialization - Automatic type resolution
  3. JsonPath Integration - Reflective field access via UTIL_JsonPath over the serialized DTO
  4. Field Comparator - Sort DTOs by any field via DTO_JsonBase.FieldComparator

Example: Basic JSON DTO

apex
public class DTO_Person extends DTO_JsonBase
{
	public String firstName;
	public String lastName;
	public Date birthDate;
	public Decimal salary;

	protected override Type getObjectType()
	{
		return DTO_Person.class;
	}
}

// Usage
DTO_Person person = new DTO_Person();
person.firstName = 'John';
person.lastName = 'Doe';
person.birthDate = Date.today();

// Serialize
String json = person.serialize();
// Output: {"firstName":"John","lastName":"Doe","birthDate":"2026-02-07"}

// Deserialize
DTO_Person deserialized = (DTO_Person)new DTO_Person().deserialize(json);

JsonPath Access:

apex
DTO_Person person = new DTO_Person();
person.firstName = 'John';

// Access fields reflectively by serializing through UTIL_JsonPath
UTIL_JsonPath path = new UTIL_JsonPath(person.serialize());
String firstName = path.findNode('firstName').getStringValue();
// firstName: John

Important: Override getObjectType() for Private Classes

If your DTO class is private or not global, you must override getObjectType():

apex
// Private DTO class
private class DTO_InternalData extends DTO_JsonBase
{
	public String data;

	// REQUIRED for private classes
	protected override Type getObjectType()
	{
		return DTO_InternalData.class;
	}
}

For global or public classes, this override is optional (auto-resolved).


Built-in DTO Classes

DTO_NameValue

Purpose: Simple DTO for a single name-value pair, designed for use in Flow invocable methods, Aura components, and LWC.

Common Use Cases:

  • Email template merge fields in Flow
  • Parameter passing to invocable methods
  • Configuration key-value pairs
  • Dynamic field mapping
apex
/**
 * @description Using DTO_NameValue in an invocable method
 */
@SuppressWarnings('PMD.AvoidGlobalModifier')
global inherited sharing class FLOW_SendEmailWithMergeFields
{
	@InvocableMethod(Category='Email' Label='Send Email with Merge Fields' Description='Sends an email using merge field values.')
	global static void execute(List<DTO_Request> requests)
	{
		if(requests == null || requests.size() != 1)
		{
			throw new IllegalArgumentException('FLOW_SendEmailWithMergeFields expects a single request');
		}
		DTO_Request request = requests.iterator().next();

		for(DTO_NameValue mergeField : request.mergeFields)
		{
			LOG_Builder.build().info('Field: ' + mergeField.name + ' = ' + mergeField.value).emitAt('FLOW_SendEmailWithMergeFields.execute');
		}
	}

	global inherited sharing class DTO_Request
	{
		@InvocableVariable(Label='Template Name' Description='The email template name' Required=true)
		global String templateName;

		@InvocableVariable(Label='Merge Fields' Description='Name-value pairs for merge fields')
		global List<DTO_NameValue> mergeFields;
	}
}

Flow Usage:

In Flow Builder, DTO_NameValue appears as a structured input allowing users to specify name-value pairs:

PropertyTypeRequiredDescription
nameStringYesThe field name or placeholder key
valueStringNoThe value to associate with the name

Creating in Apex:

apex
// Create a single name-value pair
DTO_NameValue mergeField = new DTO_NameValue();
mergeField.name = 'recipientName';
mergeField.value = 'John Doe';

// Create a list for invocable methods
List<DTO_NameValue> mergeFields = new List<DTO_NameValue>();
mergeFields.add(mergeField);

DTO_NameValue vs DTO_NameValues:

FeatureDTO_NameValueDTO_NameValues
StructureSingle name-value pairCollection of pairs
Flow Support@InvocableVariableNot directly invocable
Aura/LWC Support@AuraEnabled@AuraEnabled
Use CaseFlow inputs, simple paramsComplex parameter maps
InheritanceStandalone classExtends DTO_JsonBase

DTO_NameValues

Purpose: Store and manipulate key-value pairs for parameter passing.

Common Use Cases:

  • API request parameters
  • Dynamic configuration
  • Flow variable collections
  • Email merge fields
apex
/**
 * @description Create and use DTO_NameValues
 */
public static void demonstrateNameValues()
{
	// Create from scratch
	DTO_NameValues params = new DTO_NameValues();
	params.add('firstName', 'John');
	params.add('lastName', 'Doe');
	params.add('email', 'john.doe@example.com');

	// Create from delimited string
	DTO_NameValues params2 = new DTO_NameValues('key1=value1,key2=value2');

	// Create from map
	Map<String, String> configMap = new Map<String, String>
	{
		'timeout' => '30000',
		'retryCount' => '3'
	};
	DTO_NameValues config = new DTO_NameValues(configMap);

	// Check existence
	if(params.exists('email'))
	{
		String email = params.get('email');
	}

	// Check multiple required parameters
	Set<String> requiredParams = new Set<String>{'firstName', 'lastName'};
	if(params.allExists(requiredParams, true)) // true = must be non-blank
	{
		// All required parameters present
	}

	// Serialize to JSON
	String json = params.serialize();

	// Convert to parameter string
	String paramString = params.toParameterString();
	// Output: firstName=John,lastName=Doe,email=john.doe@example.com
}

DTO_NameValues Properties

PropertyTypeDescription
sizeIntegerNumber of name-value pairs
namesSet<String>All parameter names
valuesList<String>All parameter values

DTO_NameValues Methods

MethodDescription
add(String name, String value)Add or update a parameter
get(String name)Get parameter value (null if not found)
exists(String name)Check if parameter exists
exists(String name, Boolean isNonBlank)Check existence and optionally validate non-blank
allExists(Set<String> names)Check if all parameters exist
isEmpty()Check if no parameters exist
toParameterString()Convert to "name=value,name=value" format

DTO_BaseTable

Purpose: Structure data for Lightning datatable components.

Features:

apex
/**
 * @description Build table data for LWC datatable
 */
@AuraEnabled
public static DTO_BaseTable getAccountTable()
{
	DTO_BaseTable table = new DTO_BaseTable();

	// Define columns (label, fieldName, type, sortable)
	table.addColumn('Account Name', 'name', 'text', true);
	table.addColumn('Industry', 'industry', 'text', true);
	table.addColumn('Annual Revenue', 'revenue', 'currency', true);
	table.addColumn('Website', 'website', 'url', false);
	table.addColumn('Active', 'isActive', 'boolean', false);

	// Add rows (can be any object type)
	List<Account> accounts = QRY_Builder.selectFrom(Account.SObjectType)
		.fields(new List<SObjectField>{Account.Name, Account.Industry, Account.AnnualRevenue, Account.Website})
		.withLimit(10)
		.toList();

	for(Account account : accounts)
	{
		DTO_AccountRow row = new DTO_AccountRow();
		row.name = account.Name;
		row.industry = account.Industry;
		row.revenue = account.AnnualRevenue;
		row.website = account.Website;
		row.isActive = true;

		table.addRow(row);
	}

	return table;
}

/**
 * @description Row DTO for table data
 */
public class DTO_AccountRow
{
	@AuraEnabled
	public String name;
	@AuraEnabled
	public String industry;
	@AuraEnabled
	public Decimal revenue;
	@AuraEnabled
	public String website;
	@AuraEnabled
	public Boolean isActive;
}

Lightning Datatable Column Types

TypeDescriptionExample
textPlain textAccount Name
numberNumeric value12345
currencyCurrency formatted$50,000
percentPercentage25%
dateDate only2026-02-07
date-localDate (no timezone)2026-02-07
emailEmail addressuser@example.com
phonePhone number(555) 123-4567
urlHyperlinkhttps://example.com
booleanCheckboxtrue/false
buttonButton-
button-iconIcon button-
actionRow actions-

LWC Usage

javascript
// accountTable.js
import { wire } from 'lwc';
import { ComponentBuilder } from 'c/componentBuilder';
import getAccountTable from '@salesforce/apex/AccountController.getAccountTable';

export default class AccountTable extends ComponentBuilder('controller')
{
	tableData;

	@wire(getAccountTable)
	wiredTable({ error, data })
	{
		if(data)
		{
			this.tableData = data;
		}
	}
}
html
<!-- accountTable.html -->
<template>
    <lightning-datatable
        key-field="name"
        data={tableData.rows}
        columns={tableData.columns}
        hide-checkbox-column>
    </lightning-datatable>
</template>

DTO_PickList

Purpose: Represent picklist field metadata for dynamic UI components.

Structure:

  • DTO_PickList - Container for picklist field

    • picklistName - Field API name
    • defaultValue - Default DTO_PicklistValue
    • values - List of DTO_PicklistValue objects
  • DTO_PicklistValue - Individual picklist entry

    • label - Display text
    • value - API value
    • validFor - Controlling field dependencies

Example: Using with Invocable Method

apex
/**
 * @description Get picklist values for Flow
 */
@InvocableMethod(Label='Get Picklist Values' Description='Returns picklist values for a field')
public static List<DTO_PickList> getPicklistValues(List<Request> requests)
{
	List<DTO_PickList> results = new List<DTO_PickList>();

	for(Request req : requests)
	{
		UTIL_SObjectDescribe describe = UTIL_SObjectDescribe.getDescribe(req.objectName);
		Schema.DescribeFieldResult fieldDescribe = describe.getFieldDescribe(req.fieldName);

		DTO_PickList pickList = new DTO_PickList();
		pickList.picklistName = req.fieldName;
		pickList.values = new List<DTO_PicklistValue>();

		for(Schema.PicklistEntry entry : fieldDescribe.getPicklistValues())
		{
			if(entry.isActive())
			{
				DTO_PicklistValue pickValue = new DTO_PicklistValue();
				pickValue.label = entry.getLabel();
				pickValue.value = entry.getValue();

				if(entry.isDefaultValue())
				{
					pickList.defaultValue = pickValue;
				}

				pickList.values.add(pickValue);
			}
		}

		results.add(pickList);
	}

	return results;
}

public class Request
{
	@InvocableVariable(Required=true)
	public String objectName;

	@InvocableVariable(Required=true)
	public String fieldName;
}

DTO_ChangeEventHeader

Purpose: Carry the supported subset of a Change Data Capture event's ChangeEventHeader into Flow as a strongly-typed, Apex-defined input variable, so change-event-triggered flows can read commit and change metadata without writing bridging Apex.

Structure: every field is @AuraEnabled, so it is readable from Flow and LWC.

FieldTypeDescription
entityNameStringAPI name of the changed entity (for example Account)
recordIdsList<String>Ids of the records affected by the change
changeTypeStringThe change operation (for example CREATE, UPDATE, DELETE, UNDELETE)
changeOriginStringOrigin of the change (the client or integration that made it)
transactionKeyStringIdentifier shared by all changes committed in the same transaction
sequenceNumberIntegerPosition of this change within its transaction
commitTimestampLongCommit time as epoch milliseconds
commitUserStringId of the user who committed the change
commitNumberLongSystem change number of the commit
nulledFieldsList<String>Fields the change explicitly set to null
diffFieldsList<String>Large text fields delivered as diffs rather than full values
changedFieldsList<String>Fields whose values changed

Usage: the framework populates this DTO automatically for change-event-triggered flows — it has a copy constructor from EventBus.ChangeEventHeader — so no bridging Apex is required. See the Triggers - Guide Change Data Capture section for the end-to-end setup, and reference/apex/DTO_ChangeEventHeader.md for the complete member list.


Creating Custom DTO Classes

Creating JSON DTOs

CRITICAL: Managed Package Requirement

When extending DTOs from a managed package (e.g., DTO_JsonBase), you MUST add the @JsonAccess annotation to your DTO class. This annotation grants the managed package code permission to serialize and deserialize your subscriber org's DTO classes.

Without @JsonAccess, serialization/deserialization will fail with security errors.

apex
// CORRECT: @JsonAccess annotation required for managed package DTOs
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_CustomerOrder extends DTO_JsonBase
{
	// ...
}

// WRONG: Missing @JsonAccess annotation
public class DTO_CustomerOrder extends DTO_JsonBase
{
	// Will fail at runtime when managed package tries to serialize!
}

When to use @JsonAccess:

  • Serializable='always' - Required when managed package code calls serialize() on your DTO
  • Deserializable='always' - Required when managed package code calls deserialize() on your DTO
  • Use both when DTOs are used in bidirectional scenarios (request + response)

Type Resolution: CRITICAL Requirement for Subscriber Orgs

CRITICAL WARNING: When using KernDX as a managed package, subscriber orgs MUST implement type resolution for DTOs. Without this, the managed package cannot dynamically instantiate subscriber org classes, causing runtime failures.

Where Type Resolution Is Required:

  • DTO Deserialization - DTO_JsonBase.deserialize()
  • Dynamic Class Instantiation - Framework code using Type.forName()
  • Polymorphic Collections - Factory patterns and dynamic type creation

You MUST choose ONE of these three approaches:

  1. Make all DTOs global - Global classes are always visible to managed package code (simple but exposes classes)
  2. Implement getObjectType() in every DTO - Override method to return the class type (repetitive but explicit)
  3. Register a Type Resolver (RECOMMENDED) - Automatic type resolution for all classes (flexible and maintainable)

Without one of these solutions, you will encounter runtime errors:

text
System.JSONException: Type cannot be deserialized as it is not globally visible - DTO_CustomerRequest

Instead of overriding getObjectType() in every DTO or making all classes global, subscriber orgs can register a custom type resolver that handles type resolution for all DTOs automatically.

Benefits:

  • Eliminate repetitive getObjectType() overrides in every DTO
  • No need to make classes global (better encapsulation)
  • Centralized type resolution logic
  • Easier maintenance and less boilerplate code

How it works:

  1. Create a custom type resolver class:
apex
/**
 * @description Custom type resolver for subscriber org DTOs
 *
 * @see UTIL_TypeResolver
 */
global with sharing class CustomDTOTypeResolver extends kern.UTIL_TypeResolver.BaseClassResolver
{
	/**
	 * @description Resolves a Type object from a class name
	 *
	 * @param className The name of the class to resolve
	 *
	 * @return Type The resolved Type object or null if not found
	 */
	public override Type resolveType(String className)
	{
		return getTypeForClassName(className) ?? (Type)nextResolver?.resolveType(className);
	}

	/**
	 * @description Resolves the Type for a given class name, handling namespaces and nested classes
	 *
	 * @param className The class name to resolve
	 *
	 * @return The resolved Type object, or null if not found
	 */
	private static Type getTypeForClassName(String className)
	{
		Type classType;

		if(String.isNotBlank(className))
		{
			String namespace = kern.UTIL_System.getNamespacePrefix(
				kern.UTIL_System.getClassNamespace(className),
				'.'
			);

			classType = Type.forName(namespace, className);
			// Retry without namespace for nested classes (e.g., MyParentClass.MyChildClass)
			classType = classType == null && String.isNotBlank(namespace)
				? Type.forName('', className)
				: classType;
		}

		return classType;
	}
}
  1. Register the resolver in custom metadata:

Create a ClassTypeResolver__mdt record:

  • Label: Custom DTO Type Resolver
  • DeveloperName: CustomDTOTypeResolver
  • ClassName__c: CustomDTOTypeResolver
  1. Simplify your DTOs:
apex
// WITH Type Resolver: No getObjectType() override needed!
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_CustomerOrder extends DTO_JsonBase
{
	@AuraEnabled
	public String orderId;

	@AuraEnabled
	public String customerName;

	// No getObjectType() method needed!
}

// WITHOUT Type Resolver: Must override in every DTO
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_CustomerOrder extends DTO_JsonBase
{
	@AuraEnabled
	public String orderId;

	protected override Type getObjectType()  // Repetitive boilerplate
	{
		return DTO_CustomerOrder.class;
	}
}

When to use Type Resolver:

  • Subscriber orgs with many custom DTOs (reduces boilerplate)
  • Teams wanting centralized type resolution logic
  • Complex namespace scenarios

When to override getObjectType():

  • Single DTO or small number of DTOs
  • Private inner classes requiring specific resolution
  • No custom metadata configuration desired

Step 1: Define DTO Structure

apex
/**
 * @description Custom DTO for complex data structure
 *
 * IMPORTANT: @JsonAccess required because this extends managed package class DTO_JsonBase
 */
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_CustomerOrder extends DTO_JsonBase
{
	@AuraEnabled
	public String orderId;

	@AuraEnabled
	public String customerName;

	@AuraEnabled
	public Date orderDate;

	@AuraEnabled
	public Decimal totalAmount;

	@AuraEnabled
	public String status;

	@AuraEnabled
	public DTO_Address shippingAddress;

	@AuraEnabled
	public List<DTO_OrderItem> lineItems;

	/**
	 * @description Constructor initializes collections
	 */
	public DTO_CustomerOrder()
	{
		lineItems = new List<DTO_OrderItem>();
	}

	/**
	 * @description Required for deserialization of private classes
	 *
	 * NOTE: This override is only required if your DTO class is private/not globally visible.
	 * If you register a custom type resolver via UTIL_TypeResolver and ClassTypeResolver__mdt,
	 * you can skip implementing this method in all your DTOs.
	 *
	 * @see UTIL_TypeResolver
	 * @see ClassTypeResolver__mdt
	 */
	protected override Type getObjectType()
	{
		return DTO_CustomerOrder.class;
	}
}

/**
 * @description Nested DTO for address
 *
 * IMPORTANT: @JsonAccess required for ALL nested DTOs extending managed package classes
 *
 * NOTE: getObjectType() override optional if custom type resolver registered in ClassTypeResolver__mdt
 */
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_Address extends DTO_JsonBase
{
	@AuraEnabled
	public String street;

	@AuraEnabled
	public String city;

	@AuraEnabled
	public String state;

	@AuraEnabled
	public String postalCode;

	protected override Type getObjectType()
	{
		return DTO_Address.class;
	}
}

/**
 * @description Nested DTO for line items
 *
 * IMPORTANT: @JsonAccess required for ALL nested DTOs extending managed package classes
 *
 * NOTE: getObjectType() override optional if custom type resolver registered in ClassTypeResolver__mdt
 */
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_OrderItem extends DTO_JsonBase
{
	@AuraEnabled
	public String productName;

	@AuraEnabled
	public Integer quantity;

	@AuraEnabled
	public Decimal unitPrice;

	@AuraEnabled
	public Decimal totalPrice;

	protected override Type getObjectType()
	{
		return DTO_OrderItem.class;
	}
}

Step 2: Use the DTO

apex
// Create DTO
DTO_CustomerOrder order = new DTO_CustomerOrder();
order.orderId = 'ORD-12345';
order.customerName = 'John Doe';
order.orderDate = Date.today();
order.status = 'Processing';

// Add shipping address
DTO_Address address = new DTO_Address();
address.street = '123 Main St';
address.city = 'San Francisco';
address.state = 'CA';
address.postalCode = '94105';
order.shippingAddress = address;

// Add line items
DTO_OrderItem item1 = new DTO_OrderItem();
item1.productName = 'Widget A';
item1.quantity = 2;
item1.unitPrice = 25.00;
item1.totalPrice = 50.00;
order.lineItems.add(item1);

DTO_OrderItem item2 = new DTO_OrderItem();
item2.productName = 'Widget B';
item2.quantity = 1;
item2.unitPrice = 75.00;
item2.totalPrice = 75.00;
order.lineItems.add(item2);

order.totalAmount = 125.00;

// Serialize
String json = order.serialize();

// Deserialize
DTO_CustomerOrder parsedOrder = (DTO_CustomerOrder)
	new DTO_CustomerOrder().deserialize(json);

Implementing populate() Methods

The populate() method loads DTO data from a Salesforce record ID.

Use Case: Lazy-load DTO data from database.

CRITICAL: populate() methods MUST use SEL_Base selectors or QRY_Builder, not inline SOQL. This ensures:

  • Centralized field management
  • Reusable query logic
  • Easier testing and mocking
  • Bulk-safe patterns
  • Framework convention compliance
apex
/**
 * @description Account DTO with populate implementation
 */
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_Account extends DTO_JsonBase
{
	public String accountName;
	public String industry;
	public Decimal annualRevenue;
	public Integer employeeCount;
	public List<DTO_Contact> contacts;

	public DTO_Account()
	{
		contacts = new List<DTO_Contact>();
	}

	/**
	 * @description Populate DTO from Account ID
	 *
	 * @param recordId The Account record ID
	 */
	global override void populate(Id recordId)
	{
		populate(recordId, null);
	}

	/**
	 * @description Populate with optional parameters
	 *
	 * @param recordId The Account record ID
	 * @param dtoRequestParameters Optional parameters controlling population behavior
	 */
	global override void populate(Id recordId, DTO_NameValues dtoRequestParameters)
	{
		// CORRECT: Use SEL_* selector pattern
		Account account = (Account)new SEL_Accounts().findById(recordId);

		if(account == null)
		{
			LOG_Builder.build()
				.error('Account not found: ' + recordId)
				.emitAt('DTO_Account.populate');
			return;
		}

		// Populate fields
		this.accountName = account.Name;
		this.industry = account.Industry;
		this.annualRevenue = account.AnnualRevenue;
		this.employeeCount = account.NumberOfEmployees;

		// Check optional parameters
		Boolean includeContacts = true;
		if(dtoRequestParameters != null && dtoRequestParameters.exists('includeContacts'))
		{
			includeContacts = Boolean.valueOf(dtoRequestParameters.get('includeContacts'));
		}

		// Populate related contacts
		if(includeContacts && account.Contacts != null)
		{
			for(Contact contact : account.Contacts)
			{
				DTO_Contact contactDto = new DTO_Contact();
				contactDto.firstName = contact.FirstName;
				contactDto.lastName = contact.LastName;
				contactDto.email = contact.Email;
				this.contacts.add(contactDto);
			}
		}
	}
}

// Usage
DTO_Account accountDto = new DTO_Account();
accountDto.populate(accountId);

// With parameters
DTO_NameValues params = new DTO_NameValues();
params.add('includeContacts', 'false');
accountDto.populate(accountId, params);

Implementing transform() Methods

The transform() method converts between different DTO formats.

Use Case: Transform internal DTO to external API format.

apex
/**
 * @description Internal Account DTO
 */
public class DTO_AccountInternal extends DTO_JsonBase
{
	public String name;
	public String industryCode;
	public Decimal revenue;

	protected override Type getObjectType()
	{
		return DTO_AccountInternal.class;
	}
}

/**
 * @description External API Account DTO
 */
public class DTO_AccountExternal extends DTO_JsonBase
{
	public String companyName;
	public String industry;
	public String revenueCategory;

	/**
	 * @description Transform internal DTO to external format
	 */
	global override void transform(DTO_Base dtoBase)
	{
		if(dtoBase instanceof DTO_AccountInternal)
		{
			DTO_AccountInternal internal = (DTO_AccountInternal)dtoBase;

			// Map fields
			this.companyName = internal.name;

			// Transform industry code to readable name
			this.industry = getIndustryName(internal.industryCode);

			// Categorize revenue
			this.revenueCategory = categorizeRevenue(internal.revenue);
		}
	}

	private String getIndustryName(String code)
	{
		Map<String, String> industryMap = new Map<String, String>
		{
			'TECH' => 'Technology',
			'FIN' => 'Financial Services',
			'HEALTH' => 'Healthcare'
		};
		return industryMap.get(code);
	}

	private String categorizeRevenue(Decimal revenue)
	{
		if(revenue == null) return 'Unknown';
		if(revenue < 1000000) return 'Small';
		if(revenue < 10000000) return 'Medium';
		return 'Large';
	}

	protected override Type getObjectType()
	{
		return DTO_AccountExternal.class;
	}
}

// Usage
DTO_AccountInternal internalDto = new DTO_AccountInternal();
internalDto.name = 'Acme Corp';
internalDto.industryCode = 'TECH';
internalDto.revenue = 5000000;

DTO_AccountExternal externalDto = new DTO_AccountExternal();
externalDto.transform(internalDto);

// externalDto.companyName: Acme Corp
// externalDto.industry: Technology
// externalDto.revenueCategory: Medium

Advanced DTO Patterns

JsonPath for Reflective Access

Use UTIL_JsonPath over a serialized DTO to access fields dynamically. Each node exposes typed getters (getStringValue(), getIntegerValue(), getDecimalValue(), getBooleanValue(), getDateValue(), getDatetimeValue(), getIdValue()) — there is no untyped getValue().

apex
/**
 * @description Demonstrate JsonPath usage
 */
public static void demonstrateJsonPath()
{
	DTO_CustomerOrder order = new DTO_CustomerOrder();
	order.orderId = 'ORD-12345';
	order.customerName = 'John Doe';
	order.totalAmount = 125.00;

	DTO_OrderItem item = new DTO_OrderItem();
	item.productName = 'Widget A';
	item.quantity = 2;
	item.unitPrice = 25.00;
	order.lineItems.add(item);

	// Wrap the serialized DTO in UTIL_JsonPath for path-based reads
	UTIL_JsonPath path = new UTIL_JsonPath(order.serialize());

	// Access fields by path (use the typed getter that matches the underlying field)
	String orderId = path.findNode('orderId').getStringValue();
	// orderId: ORD-12345

	// Access nested fields
	String productName = path.findNode('lineItems[0].productName').getStringValue();
	// productName: Widget A

	// Check field existence
	if(path.exists('totalAmount'))
	{
		Decimal total = path.findNode('totalAmount').getDecimalValue();
	}
}

Common JsonPath Patterns:

PatternDescriptionExample
fieldNameTop-level fieldpath.findNode('orderId')
parent.childNested objectpath.findNode('shippingAddress.city')
array[0]Array indexpath.findNode('lineItems[0]')
array[0].fieldArray element fieldpath.findNode('lineItems[0].productName')

Sorting DTOs with FieldComparator

Sort DTO collections by any field using DTO_JsonBase.FieldComparator.

apex
/**
 * @description Sort DTOs by field values
 */
public static void demonstrateSorting()
{
	List<DTO_CustomerOrder> orders = new List<DTO_CustomerOrder>();

	// Create sample orders
	for(Integer i = 0; i < 5; i++)
	{
		DTO_CustomerOrder order = new DTO_CustomerOrder();
		order.orderId = 'ORD-' + i;
		order.customerName = 'Customer ' + i;
		order.totalAmount = Math.random() * 1000;
		order.orderDate = Date.today().addDays(-i);
		orders.add(order);
	}

	// Sort by total amount (ascending)
	DTO_JsonBase.FieldComparator amountComparator =
		new DTO_JsonBase.FieldComparator('totalAmount', true);
	orders.sort(amountComparator);

	// Sort by customer name (descending)
	DTO_JsonBase.FieldComparator nameComparator =
		new DTO_JsonBase.FieldComparator('customerName', false);
	orders.sort(nameComparator);

	// Sort by date
	DTO_JsonBase.FieldComparator dateComparator =
		new DTO_JsonBase.FieldComparator('orderDate', true);
	orders.sort(dateComparator);
}

DTO Collections and Equality

DTOs implement equals() and hashCode() for collection support.

apex
/**
 * @description Demonstrate DTO collections
 */
public static void demonstrateCollections()
{
	DTO_CustomerOrder order1 = new DTO_CustomerOrder();
	order1.orderId = 'ORD-001';
	order1.totalAmount = 100.00;

	DTO_CustomerOrder order2 = new DTO_CustomerOrder();
	order2.orderId = 'ORD-001';
	order2.totalAmount = 100.00;

	DTO_CustomerOrder order3 = new DTO_CustomerOrder();
	order3.orderId = 'ORD-002';
	order3.totalAmount = 200.00;

	// Equality based on serialized JSON
	Boolean areEqual = order1.equals(order2); // true (same content)
	Boolean areDifferent = order1.equals(order3); // false (different content)

	// Use in Sets (duplicates removed)
	Set<DTO_JsonBase> uniqueOrders = new Set<DTO_JsonBase>();
	uniqueOrders.add(order1);
	uniqueOrders.add(order2); // Duplicate, not added
	uniqueOrders.add(order3);
	// uniqueOrders.size(): 2

	// Use as Map keys
	Map<DTO_JsonBase, String> orderMap = new Map<DTO_JsonBase, String>();
	orderMap.put(order1, 'Processing');
	orderMap.put(order2, 'Shipped'); // Overwrites order1
	orderMap.put(order3, 'Delivered');
	// orderMap.size(): 2
}

Integration Patterns

DTOs in REST APIs

Complete example showing inbound REST API with DTOs using the API_Inbound framework.

apex
/**
 * @description REST endpoint for order management
 */
@RestResource(UrlMapping='/v1/orders/*')
global inherited sharing class REST_Orders
{
	@HttpPost
	global static void createOrder()
	{
		API_Dispatcher.processInboundService(API_CreateOrder.class.getName());
	}
}

/**
 * @description Create order API implementation
 */
public with sharing class API_CreateOrder extends API_Inbound
{
	private Foobar__c order;

	public override void configure()
	{
		super.configure();
		requestPayload = new DTO_Request();
		responsePayload = new DTO_Response();
	}

	public override List<String> getValidationErrors()
	{
		List<String> errors = new List<String>();
		DTO_Request dto = (DTO_Request)requestPayload;

		if(String.isBlank(dto.customerName))
		{
			errors.add('Customer name is required');
		}

		return errors;
	}

	public override void onSuccess()
	{
		super.onSuccess();
		DTO_Request dto = (DTO_Request)requestPayload;

		order = new Foobar__c();
		order.Name = dto.customerName;

		doInsert(order);
	}

	public override void updateResponseDTO()
	{
		DTO_Response dto = (DTO_Response)responsePayload;

		if(result.isSuccess)
		{
			dto.success = true;
			dto.orderId = order.Id;
			dto.message = 'Order created successfully';
		}
	}

	@JsonAccess(Deserializable='always')
	public class DTO_Request extends DTO_JsonBase
	{
		/**
		 * @description The customer name.
		 */
		public String customerName;

		/**
		 * @description The order date.
		 */
		public Date orderDate;

		/**
		 * @description The total amount.
		 */
		public Decimal totalAmount;
	}

	@JsonAccess(Serializable='always')
	public class DTO_Response extends DTO_JsonBase
	{
		/**
		 * @description Whether the operation was successful.
		 */
		public Boolean success;

		/**
		 * @description The created order ID.
		 */
		public String orderId;

		/**
		 * @description A status message.
		 */
		public String message;
	}
}

DTOs in LWC Components

apex
/**
 * @description LWC controller for customer dashboard
 */
public with sharing class CustomerDashboardController
{
	/**
	 * @description Get customer summary data
	 */
	@AuraEnabled(Cacheable=true)
	public static String getCustomerSummary(Id customerId)
	{
		Account customer = (Account)new SEL_Accounts().findById(customerId);

		DTO_CustomerSummary summary = new DTO_CustomerSummary();
		summary.customerName = customer.Name;
		summary.industry = customer.Industry;
		summary.revenue = customer.AnnualRevenue;
		summary.phone = customer.Phone;

		return summary.serialize();
	}
}

/**
 * @description Customer summary DTO
 */
@JsonAccess(Serializable='always' Deserializable='always')
public class DTO_CustomerSummary extends DTO_JsonBase
{
	@AuraEnabled
	public String customerName;

	@AuraEnabled
	public String industry;

	@AuraEnabled
	public Decimal revenue;

	@AuraEnabled
	public String phone;

	protected override Type getObjectType()
	{
		return DTO_CustomerSummary.class;
	}
}

LWC JavaScript:

javascript
import { api, wire } from 'lwc';
import { ComponentBuilder } from 'c/componentBuilder';
import getCustomerSummary from '@salesforce/apex/CustomerDashboardController.getCustomerSummary';

export default class CustomerDashboard extends ComponentBuilder('controller')
{
	@api recordId;
	customerData;

	@wire(getCustomerSummary, { customerId: '$recordId' })
	wiredCustomer({ error, data })
	{
		if(data)
		{
			this.customerData = JSON.parse(data);
		}
	}
}

Passing Complex DTOs as @AuraEnabled Parameters

Never pass a complex DTO directly as an @AuraEnabled method parameter. LWC Proxy-wraps reactive objects before serializing them, and Aura's parameter deserializer rejects the Proxy wrapper when the target type is a DTO extending DTO_JsonBase (or any class with nested collections). The callout fails at runtime with messages such as "Unable to deserialize to specified type" or "cannot be deserialized from non-object type".

The working pattern is to accept a String requestJson parameter on the controller and deserialize it with JSON.deserialize(...) inside the method. The client stringifies the DTO before calling. This is the pattern used throughout the framework (see CTRL_ScheduledJob.saveRecord).

apex
// CORRECT — accept String, deserialize server-side
@AuraEnabled
public static Id saveRecord(String requestJson)
{
	SaveRequest request = (SaveRequest)JSON.deserialize(requestJson, SaveRequest.class);
	// ... use request.className, request.cronExpression, ...
}

// WRONG — passing the DTO directly fails with LWC Proxy
@AuraEnabled
public static Id saveRecord(SaveRequest request) { /* runtime deserialization error */ }
javascript
// LWC caller stringifies before invoking
await this.callControllerMethod('saveRecord', {requestJson: JSON.stringify(this.request)});

This restriction applies only to input parameters. Return values from @AuraEnabled methods can be complex DTOs — Aura serializes DTO instances to JSON correctly on the way out, and LWC consumes them as plain JavaScript objects.


DTOs in Flow Invocables

apex
/**
 * @description Send email with dynamic merge fields (Flow invocable)
 */
@SuppressWarnings('PMD.AvoidGlobalModifier')
global inherited sharing class FLOW_SendCustomEmail
{
	@InvocableMethod(Category='Email' Label='Send Custom Email' Description='Sends an email with dynamic merge fields.')
	global static List<DTO_Response> execute(List<DTO_Request> requests)
	{
		if(requests == null || requests.size() != 1)
		{
			throw new IllegalArgumentException('FLOW_SendCustomEmail expects a single request');
		}
		DTO_Request request = requests.iterator().next();

		DTO_Response response = new DTO_Response();

		try
		{
			String emailBody = request.templateBody;

			for(DTO_NameValue mergeField : request.mergeFields)
			{
				String placeholder = '{!' + mergeField.name + '}';
				emailBody = emailBody.replace(placeholder, mergeField.value);
			}

			Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
			email.setToAddresses(new List<String>{request.recipientEmail});
			email.setSubject(request.subject);
			email.setPlainTextBody(emailBody);

			Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{email});

			response.success = true;
			response.message = 'Email sent successfully';
		}
		catch(Exception error)
		{
			response.success = false;
			response.message = 'Error: ' + error.getMessage();
			LOG_Builder.build().error(error).emitAt('FLOW_SendCustomEmail.execute');
		}

		return new List<DTO_Response> {response};
	}

	global inherited sharing class DTO_Request
	{
		@InvocableVariable(Label='Recipient Email' Description='The email address to send to' Required=true)
		global String recipientEmail;

		@InvocableVariable(Label='Subject' Description='The email subject line' Required=true)
		global String subject;

		@InvocableVariable(Label='Template Body' Description='The email body template with merge field placeholders' Required=true)
		global String templateBody;

		@InvocableVariable(Label='Merge Fields' Description='Name-value pairs for template merge fields')
		global List<DTO_NameValue> mergeFields;
	}

	global inherited sharing class DTO_Response
	{
		@InvocableVariable(Label='Success' Description='Whether the email was sent successfully')
		global Boolean success;

		@InvocableVariable(Label='Message' Description='Status or error message')
		global String message;
	}
}

Testing

DTOs are tested primarily through their integration points: web service tests, LWC controller tests, and Flow invocable tests. The framework's base classes (DTO_Base, DTO_JsonBase) are tested by the framework itself, so your tests should focus on custom DTO behaviour such as populate() and transform() overrides.

Testing serialize/deserialize round-trips:

apex
@IsTest
private static void shouldSerializeAndDeserializeSuccessfully()
{
	DTO_OrderSummary original = new DTO_OrderSummary();
	original.orderId = 'ORD-001';
	original.totalAmount = 250.00;

	String json = original.serialize();
	DTO_OrderSummary deserialized = (DTO_OrderSummary)new DTO_OrderSummary().deserialize(json);

	Assert.areEqual(original.orderId, deserialized.orderId, 'Order ID should survive round-trip');
	Assert.areEqual(original.totalAmount, deserialized.totalAmount, 'Total amount should survive round-trip');
}

Testing populate() with TST_Mock:

When your DTO's populate() method queries records via selectors, use TST_Mock to register mock data for DML-free testing:

apex
@IsTest
private static void shouldPopulateFromRecord()
{
	Foobar__c mock = (Foobar__c)TST_Mock.of(Foobar__c.SObjectType)
		.withOverride(Foobar__c.Email__c, 'test@example.com')
		.build();

	DTO_Request dto = new DTO_Request();
	dto.populate(mock.Id, null);

	Assert.areEqual('test@example.com', dto.email, 'Email should be populated from record');

	TST_Mock.clear();
}

Testing transform():

apex
@IsTest
private static void shouldTransformBetweenFormats()
{
	DTO_AccountInternal internal = new DTO_AccountInternal();
	internal.name = 'Acme Corp';
	internal.industryCode = 'TECH';

	DTO_AccountExternal external = new DTO_AccountExternal();
	external.transform(internal);

	Assert.areEqual('Acme Corp', external.companyName, 'Company name should be mapped');
	Assert.areEqual('Technology', external.industry, 'Industry code should be translated');
}

Testing DTOs in web service context:

For DTOs used in API classes, testing happens through API_OutboundTestHelper assertions. The framework handles DTO population and serialization internally, so your test verifies the end-to-end flow rather than the DTO in isolation. See Web Services - Guide for patterns.


Anti-Patterns

Anti-PatternWhy It's WrongInstead
Missing @JsonAccess on DTOs in subscriber orgsSerialization fails at runtime with a security error in managed package contextAlways add @JsonAccess(Serializable='always' Deserializable='always') on every DTO extending a framework base class
Business logic inside populate()Makes the DTO untestable in isolation and violates single responsibilityKeep populate() limited to data mapping; move logic to service classes or trigger actions
Skipping type resolver registration in subscriber orgsDeserialization fails with TypeException because the managed package cannot resolve subscriber class namesRegister a ClassTypeResolver__mdt record or implement a custom UTIL_TypeResolver
Using raw Map<String, Object> instead of typed DTOsNo compile-time safety, hard to refactor, error-prone key accessCreate a DTO class extending DTO_JsonBase for structured data
Null-unsafe access to DTO propertiesCauses NullPointerException at runtime when optional fields are missingUse null checks or default values before accessing optional DTO properties
Passing a complex DTO directly as an @AuraEnabled parameterLWC Proxy-wraps the object and Aura cannot deserialize it into the DTO type — fails with "Unable to deserialize to specified type"Accept String requestJson on the controller, call JSON.deserialize(...) server-side; stringify in the LWC caller (see CTRL_ScheduledJob.saveRecord)

Best Practices

Use Appropriate DTO Type

apex
// GOOD: JSON API
public class DTO_ApiRequest extends DTO_JsonBase { }

Implement getObjectType() for Private Classes (OR Use Type Resolver)

Option A: Override getObjectType() in each DTO (simple approach):

apex
// GOOD for small number of DTOs
private class DTO_InternalData extends DTO_JsonBase
{
	public String data;

	protected override Type getObjectType()
	{
		return DTO_InternalData.class;
	}
}

Option B: Register custom type resolver (recommended for 10+ DTOs):

apex
// BETTER for many DTOs - no getObjectType() needed!
// Just register CustomDTOTypeResolver in ClassTypeResolver__mdt

private class DTO_InternalData extends DTO_JsonBase
{
	public String data;
	// No getObjectType() method needed - resolved automatically!
}

See Type Resolution: CRITICAL Requirement for Subscriber Orgs for setup details.

Use @AuraEnabled for LWC

When exposing DTO fields to Lightning Web Components, annotate with @AuraEnabled:

apex
// GOOD
public class DTO_Data extends DTO_JsonBase
{
	@AuraEnabled
	public String fieldName;
}

Initialize Collections

apex
// GOOD
public class DTO_Order extends DTO_JsonBase
{
	public List<DTO_OrderItem> lineItems;

	public DTO_Order()
	{
		lineItems = new List<DTO_OrderItem>();
	}
}

Handle Null Values

apex
// GOOD
this.industry = String.isNotBlank(account.Industry) ? account.Industry : 'Unknown';

// BAD
this.industry = account.Industry.toUpperCase(); // Null pointer risk

Use Clear Naming

apex
// GOOD
public class DTO_CustomerOrderRequest extends DTO_JsonBase { }

// BAD
public class DTO_Data extends DTO_JsonBase { } // Vague

Document Complex DTOs

apex
/**
 * @description Customer order request DTO for external order management system
 */
public class DTO_CustomerOrderRequest extends DTO_JsonBase
{
	/**
	 * @description Unique order ID from external system
	 */
	public String externalOrderId;
}

Validate DTO Data

apex
public override List<String> getValidationErrors()
{
	List<String> errors = new List<String>();
	DTO_Request dto = (DTO_Request)requestPayload;

	if(String.isBlank(dto.customerName))
	{
		errors.add('Customer name is required');
	}

	return errors;
}

Keep DTOs Focused

apex
// GOOD: Focused DTO
public class DTO_AccountBasic extends DTO_JsonBase
{
	public String name;
	public String industry;
}

// BAD: Over-complicated
public class DTO_Everything extends DTO_JsonBase
{
	// Too many unrelated fields
}

Troubleshooting

Issue: "Type cannot be deserialized as it is not globally visible" or "System.JSONException: Type cannot be constructed"

Cause: Missing @JsonAccess annotation on DTO extending managed package base class.

This is the #1 most common error when using KernDX DTOs from a managed package.

Error Example:

text
System.JSONException: Type cannot be deserialized as it is not globally visible - DTO_CustomerOrder

Solution: Add @JsonAccess(Serializable='always' Deserializable='always') to your DTO class. See the full WRONG/CORRECT example in Type Resolution: CRITICAL Requirement for Subscriber Orgs.

Why this is required: When your subscriber org's code extends a managed package class (e.g., YourNamespace.DTO_JsonBase), and the managed package code tries to serialize/deserialize your class, Salesforce security requires explicit permission via @JsonAccess annotation. Without it, the operation fails with a security error.


Issue: "Unable to deserialize to specified type"

Cause: Missing getObjectType() implementation for private DTO class.

Solution Option 1 - Override getObjectType() in each DTO:

apex
protected override Type getObjectType()
{
	return DTO_MyClass.class;
}

Solution Option 2 - Register custom type resolver (recommended for 10+ DTOs):

  1. Create custom resolver (see complete implementation above in "Type Resolution" section)
  2. Register in ClassTypeResolver__mdt with ClassName__c = 'CustomDTOTypeResolver'
  3. Remove getObjectType() from all DTOs - type resolution is now automatic!

Issue: "Null pointer exception when accessing DTO fields"

Cause: Collection not initialized.

Solution:

apex
public DTO_Order()
{
	lineItems = new List<DTO_OrderItem>();
}

Issue: "DTO fields not visible in LWC"

Cause: Missing @AuraEnabled.

Solution:

apex
@AuraEnabled
public String fieldName;

Reference

DTO Framework Classes

ClassPurposeExtends
DTO_BaseAbstract base for all DTOs-
DTO_JsonBaseJSON serializationDTO_Base
DTO_NameValuesKey-value collectionsDTO_JsonBase
DTO_BaseTableLightning datatableDTO_JsonBase
DTO_PickListPicklist metadata-

Key Methods

DTO_Base:

apex
global virtual String serialize()
global virtual DTO_Base deserialize(String dtoString)
global virtual void populate(Id recordId)
global virtual void transform(DTO_Base dtoBase)