This is probably a bug, and I'd recommend contacting Salesforce Support (If you haven't already).
Heres a repo I wrote using a custom object (Test__c
, with a single time field, Some_Time__c
). It starts with the a demo using Time
as the object, then runs the same code for Test__c
. The raw Time
object works, and has the same syntax as the JSON, but the JSON object fails with the same message.
Time t = DateTime.now().Time();
String s = JSON.serialize(t);
System.debug(s); // "13:50:40.848Z"
Time t2 = (Time)JSON.deserialize(s, Time.class);
System.debug(t2); // 13:50:40.848Z
Test__c test = new Test__c(
Some_Time__c = DateTime.now().Time()
);
String testString = JSON.serialize(test);
System.debug(testString);
// {"attributes":{"type":"Test__c"},"Some_Time__c":"13:50:40.850Z"}
// "13:50:40.850Z"
Test__c result = (Test__c)JSON.deserialize(testString, Test__c.class);
Some additional notes from my test runs:
- Using
Map<String, Object> record = (Map<String, Object>)JSON.deserializeUntyped(testString);
returns a valid Time
string in the map.
- Trying to access this with
System.debug((Time)record.get('Some_Time__c'));
fails with a:
System.TypeException: Invalid conversion from runtime type String to Time
- Since theres no
Time.Parse
method, I cant figure out a good way to get the string value back into a Time
object
I spent some time writing an actual parser, which detects field types & behaves accordingly (using the fancy new switch
statement). This should cover most use cases for people experiencing this issue until salesforce releases an actual fix.
Theres a few other improvements I could make, such as using the attributtes
node to create the type, instead of using it as a parameter, and some extra null handling/error handling, but hey, it works!
public class GenericParser {
public static sObject ParseSObject(String jsonString, Type typeOf) {
JSONParser parser = JSON.createParser(jsonString);
sObject record = (sObject)typeOf.newInstance();
Map<String, Schema.SObjectField> fields = record.getsObjectType().getDescribe().fields.getMap();
while (parser.nextToken() != null) {
if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
if (fields.containsKey(parser.getText())) {
String label = parser.getText();
parser.nextValue();
Schema.SOAPType fieldType = fields.get(label).getDescribe().getSOAPType();
// Note that the enums type is not needed, just the value (TIME instead of Schema.SOAPType.TIME)
switch on fieldType {
when TIME {
record.put(label, ParseTime(parser.getText()));
} when BOOLEAN {
record.put(label, parser.getBooleanValue());
} when DOUBLE {
record.put(label, parser.getDoubleValue());
} when DATE {
record.put(label, parser.getDateValue());
} when DATETIME {
record.put(label, parser.getDateTimeValue());
} when INTEGER {
record.put(label, parser.getIntegerValue());
} when else {
record.put(label, parser.getText());
}
}
}
}
}
return record;
}
public static Time ParseTime(String timeString) {
// 14:26:41.276Z or "14:26:41.276Z"
List<String> values = timeString.replace('Z', '').replace('"', '').split(':');
// (14, 26, 41.276)
Integer hours = Integer.valueOf(values[0]);
Integer minutes = Integer.valueOf(values[1]);
// 41.276 -> (41, 276)
Integer seconds = Integer.valueOf(values[2].split('\\.')[0]);
Integer milliseconds = Integer.valueOf(values[2].split('\\.')[1]);
return Time.newInstance(hours, minutes, seconds, milliseconds);
}
}
As a bonus, it comes with a test class! I used a dummy object, with a ton of fields of just about every type, in order to ensure any field type works. Just about every Schema.SOAPType
value is represented. Youll need to replace this with one of your own objects, but the idea is to have a generic parser, so it should work with any object & any number of fields.
@isTest
public class GenericParser_test {
@isTest
public static void Test_Time() {
Time t = DateTime.now().Time();
String j = JSON.serialize(t);
Time r = GenericParser.ParseTime(j);
System.assertEquals(t, r, 'Time values should be equal');
}
@isTest
public static void Test_Parser() {
Test__c t = new Test__c(
Account__c = '001c000001GWSaL', // Dummy Id to avoid DML
Opportunity__c = '006c000000GMQYv',
Some_Time__c = DateTime.now().Time(),
Test_Checkbox__c = true,
Test_Coord__latitude__s = 0,
Test_Coord__longitude__s = 0,
Test_Currency__c = 55.75,
Test_Date__c = Date.today(),
Test_DateTime__c = DateTime.now(),
Test_Email__c = '[email protected]',
Test_MultiPicklist__c = 'Test;Test 2',
Test_Num__c = 77777755,
Test_Percent__c = 80,
Test_Picklist__c = 'Test',
Test_Rich_Text__c = '<hr /><br /><b>Test</b><p>Text</p>',
Test_TextArea__c = 'Text',
Test_Url__c = 'example.org'
);
String j = JSON.serialize(t);
sObject r = GenericParser.ParseSObject(j, Type.forName('Test__c'));
Test__c t2 = (Test__c)r;
Map<String, Object> startingValues = t.getPopulatedFieldsAsMap();
Map<String, Object> result = t2.getPopulatedFieldsAsMap();
System.assertEquals(startingValues.keySet(), result.keySet());
for (String key:startingValues.keySet()) {
System.assertEquals(startingValues.get(key), result.get(key));
}
}
}