Skip to main content

How to Parse Raw JSON Payloads in Salesforce Apex Custom REST APIs

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.

In plain words: When external systems send deeply nested JSON or lists in an HTTP request, Apex method parameters fall short. Instead, you extract the raw JSON body using 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.

JSON payload in POST apex rest

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() + '}';
    }
}
Execution Lifecycle Breakdown:
  • 1. Capture Context: RestContext.request intercepts 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.statusCode allows explicit HTTP status assignment (e.g., 200, 201, 400, 500).

3. Common Traps & Platform Limits

Developer Trap: Do not define parameters in your @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.
Apex REST Best-Practice Matrix:
  • Security: Always declare custom services as with sharing or inherited sharing and perform DML with as user or WITH USER_MODE to 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-catch blocks (JSONException) to gracefully return meaningful 400 Bad Request responses to external clients.
Core Rule: Use parameter binding only for small, flat payloads. For production integrations with nested objects or arrays, use parameterless methods combined with RestContext and JSON.deserialize.

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.