[SalesForce] Cannot deserialize instance of time from VALUE_STRING value

I'm trying to parse a Time field from my lightning component to my apex controller by using JSON and deserializing it in apex into an existing Object.

But unfortunately I'm always getting the error

Cannot deserialize instance of time from VALUE_STRING value 10:30:00.000Z

I already tried different Time formats and with/without quotes "" in my JSON

  • 10:30
  • 10:30:00
  • 10:30:00.000
  • 10:30:00.000Z
  • T10:00:00.000Z
  • 2018-06-07T10:00:00.000Z

But nothing works.

  • My Org is on Spring 18 API Version 42
  • I have an SObject (Event__c) with a Time Field

The problem is reproducable in anonymous apex with following code

Event__c workingObject = new Event__c();
workingObject.Name = 'Lorem';
workingObject.Public_Start_Time__c = Time.newInstance(10,10,10,10);

String serializedObject = JSON.serialize(workingObject);
String mixedObject = '{"Public_Start_Time__c": "' + workingObject.Public_Start_Time__c + '", "name": "Ipsum"}';
String manualObject = '{"Public_Start_Time__c": "2018-06-07T10:00:00.000Z", "name": "Dolor"}';
try {
   Event__c banana = (Event__c)JSON.deserialize(serializedObject, Event__c.class);
}
catch(Exception ex) {
    System.debug(ex.getMessage());
}

try {
   Event__c banana = (Event__c)JSON.deserialize(mixedObject, Event__c.class);
}
catch(Exception ex) {
    System.debug(ex.getMessage());
}

try {
   Event__c banana = (Event__c)JSON.deserialize(manualObject, Event__c.class);
}
catch(Exception ex) {
    System.debug(ex.getMessage());
}

which results in the 3 following messages

Cannot deserialize instance of time from VALUE_STRING value 10:10:10.010Z or request may be missing a required field

Cannot deserialize instance of time from VALUE_STRING value 10:10:10.010Z or request may be missing a required field

Cannot deserialize instance of time from VALUE_STRING value 2018-06-07T10:00:00.000Z or request may be missing a required field

Everything I found so far was for dates or datetimes but not time only.

Best Answer

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 = 'example@example.com',
            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));
        }
    }

}
Related Topic