Fixing Salesforce JSON_PARSER_ERROR on currency fields from Drupal
If Drupal's Salesforce Suite pushes a currency value such as 16,000,000 or $2,500 to a Salesforce currency field, Salesforce rejects the whole record with a JSON_PARSER_ERROR. Salesforce can coerce a numeric string like "16000000", but not one with thousands separators or a currency symbol. The fix is to normalise those values to plain numbers in a subscriber to the Salesforce Suite push params event, just before the request is built.
Where the bad values came from
The case I'm describing comes from a group of nonprofit membership sites on one Drupal distribution, where a teammate tracked it down. Members fill in Webform-based applications, and each submission is mapped to a Salesforce Opportunity through the Salesforce Suite module (salesforce_mapping and salesforce_push). Several of the mapped fields are Salesforce currency fields: amounts, donations, figures a member reports about their own organisation.
On the Drupal side, some of those were plain text fields rather than number fields, and at least one was filled in automatically from data the site already held, already formatted for display. So a value arrived at the mapping as the string 16,000,000. Salesforce Suite passes mapped values through as they are, Salesforce tries to read the currency field as a number, and the push fails.
Why the error message points the wrong way
JSON_PARSER_ERROR sounds like the request body is malformed JSON. It isn't. The body is valid JSON; it is the value of one field that Salesforce cannot parse as the type it expects. That makes it easy to spend time on encoding, character sets or the HTTP client before looking at the data. When you see this error from a push, look first at the numeric and date fields in the payload and at how their values are formatted.
It is also the kind of bug that hides for a long time. Most people type 2500, so most pushes work. The failure only appears when someone types 2,500, or when an auto-filled value is large enough to get a separator.
Why fix it at push time, not in the form
The obvious alternative is to validate the Webform fields so only plain numbers are accepted. That helps for new input, but it does not help with three things: submissions that already exist and are queued for push or will be re-pushed, values filled in by code rather than typed by a person, and every other form on other sites mapped to the same Salesforce fields.
Salesforce Suite dispatches an event with the outgoing parameters for every push, after the field mappings have run and before the API call. That is the one place every value passes through, whatever produced it. The team's fix was to subscribe to it and clean the currency fields there.
How to normalise currency values in a push params subscriber
Here is a minimal version of the pattern. It listens for SalesforceEvents::PUSH_PARAMS, checks it is the mapping it cares about, and rewrites each listed currency parameter to digits, a decimal point and an optional minus sign:
<?php
namespace Drupal\my_module\EventSubscriber;
use Drupal\salesforce\Event\SalesforceEvents;
use Drupal\salesforce_mapping\Event\SalesforcePushParamsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class CurrencyPushParamsSubscriber implements EventSubscriberInterface {
private const CURRENCY_FIELDS = ['Amount', 'Donation_Amount__c'];
public static function getSubscribedEvents(): array {
return [SalesforceEvents::PUSH_PARAMS => 'onPushParams'];
}
public function onPushParams(SalesforcePushParamsEvent $event): void {
if ($event->getMapping()->id() !== 'application_to_opportunity') {
return;
}
$params = $event->getParams();
foreach (self::CURRENCY_FIELDS as $field) {
if (!$params->hasParam($field)) {
continue;
}
$value = $params->getParam($field);
if (!is_string($value) || $value === '') {
continue;
}
$clean = preg_replace('/[^0-9.\-]/', '', $value);
if ($clean === '' || !is_numeric($clean)) {
// Better to leave the field empty than fail the whole record.
$params->unsetParam($field);
continue;
}
$params->setParam($field, $clean);
}
}
}
Register it as a service tagged event_subscriber in your module's services.yml as usual. The field names here are placeholders; use the API names from your own Salesforce object.
Two small decisions in it are worth calling out. Non-string values are left alone, so a field that is already a number is not touched. And a value that has nothing numeric left after cleaning is dropped rather than sent. An empty currency field on the Opportunity is a data-quality gap someone can fill in; a failed push loses every other field on the record too.
What this approach does not handle
Stripping everything but digits, dots and minus signs assumes the input uses a full stop as the decimal separator. A value written the European way, 1.234,56, would become 1.23456, which is a number and will push without complaint. If your users might enter amounts in that format, decide on the locale explicitly, for example with PHP's NumberFormatter::parse(), rather than relying on a regular expression.
The fix also lists its currency fields by hand. The team needed it for two different applications mapped to the same object, and each got its own list. That is fine for a handful of fields. Beyond that, I'd reach for one shared subscriber that decides which parameters to clean from the Salesforce field type, so a new currency field added to a mapping is covered without anyone remembering to update a list.
Finally, it is still worth fixing the source. Changing the Drupal fields to number fields, or validating them, means the data stored in Drupal is clean as well, not only the copy in Salesforce.
Key takeaways
- Salesforce returns
JSON_PARSER_ERRORwhen a currency or number field receives a string with thousands separators or a currency symbol, even though the JSON itself is valid. - The Salesforce Suite
PUSH_PARAMSevent sees every outgoing value after mapping, so it is the right place to normalise data that could come from forms, code or old submissions. - Dropping a value that cannot be cleaned is usually better than sending it, because one bad field fails the push for the whole record.
- Regex cleaning assumes a full stop as the decimal separator; parse with an explicit locale if users may write amounts the European way.