Exposing custom REST endpoints in Salesforce using the @RestResource annotation allows third-party systems to create, update, or query records in real time. While simple endpoints can map incoming JSON properties directly to method parameters, complex enterprise integrations often send nested JSON structures, line-item arrays, or dynamic payloads that require manual extraction and deserialization.
RestContext.request.requestBody and convert it into a strongly typed Apex wrapper class using JSON.deserialize().
1. The Basic Approach: Method Parameter Mapping
For simple flat payloads, Apex allows you to declare method arguments that match the incoming JSON keys. Salesforce automatically binds the payload fields to the method signature:
@RestResource(urlMapping='/api/v1/CreateContact/*')
global with sharing class ContactRestService {
@HttpPost
global static String doPost(String lastName, String phone) {
Contact con = new Contact(
LastName = lastName,
Phone = phone
);
insert as user con;
return con.Id;
}
}
While this works well for straightforward key-value pairs, it breaks down when external systems send complex object hierarchies, lists of records, or mismatched field types.
2. Handling Complex Payloads with RestContext
To accept advanced payloads like orders with multiple line items, use the global RestContext class to access the raw request body as a Blob, convert it to a string, and deserialize it into an Apex wrapper.
Step 1: Define the Strongly Typed Data Model
Create a wrapper class matching the schema of the incoming JSON body. You can write this manually or generate it using tools like JSON2Apex:
public with sharing class InvoicePayloadWrapper {
public List<InvoiceItem> invoiceList;
public class InvoiceItem {
public Decimal totalPrice;
public String statementDate;
public String invoiceNumber;
public List<LineItem> lineItems;
}
public class LineItem {
public Decimal unitPrice;
public Integer quantity;
public String productName;
}
public static InvoicePayloadWrapper parse(String jsonString) {
return (InvoicePayloadWrapper) JSON.deserialize(jsonString, InvoicePayloadWrapper.class);
}
}
Step 2: Implement the REST Endpoint
Extract the raw body using RestContext.request.requestBody.toString() and deserialize it directly into your typed wrapper class:
@RestResource(urlMapping='/api/v1/ProcessInvoices/*')
global with sharing class InvoiceRestService {
@HttpPost
global static String doPost() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
// 1. Extract raw body from context
Blob bodyBlob = req.requestBody;
if (bodyBlob == null) {
res.statusCode = 400;
return '{"error": "Request body cannot be empty"}';
}
String requestString = bodyBlob.toString();
// 2. Deserialize JSON into structured wrapper
InvoicePayloadWrapper parsedData = InvoicePayloadWrapper.parse(requestString);
// 3. Process business logic safely
System.debug(LoggingLevel.INFO, 'Parsed Invoices: ' + parsedData.invoiceList);
res.statusCode = 201;
return '{"status": "Success", "recordsReceived": ' + parsedData.invoiceList.size() + '}';
}
}
- 1. Capture Context:
RestContext.requestintercepts the active HTTP session. - 2. Binary to String:
req.requestBody.toString()converts the incoming byte stream (Blob) into raw JSON text. - 3. Type Cast:
JSON.deserialize()reconstructs the text into concrete Apex objects, lists, and primitives. - 4. Response Control:
RestContext.response.statusCodeallows explicit HTTP status assignment (e.g., 200, 201, 400, 500).
3. Common Traps & Platform Limits
@HttpPost method signature if you also intend to read RestContext.request.requestBody. If method parameters are declared, Salesforce consumes the request stream for parameter binding, leaving requestBody null or inaccessible.
- Security: Always declare custom services as
with sharingorinherited sharingand perform DML withas userorWITH USER_MODEto respect object and field-level permissions. - Heap Limits: Avoid parsing multi-megabyte JSON payloads in synchronous REST calls (keep payloads within the 6 MB synchronous heap limit).
- Error Handling: Wrap deserialization calls in
try-catchblocks (JSONException) to gracefully return meaningful 400 Bad Request responses to external clients.
Summary
Managing custom REST integrations in Salesforce becomes significantly more reliable when you decouple your method signature from incoming data structures. Utilizing RestContext paired with dedicated wrapper classes gives you complete control over deserialization, validation, status codes, and exception handling.