0

I have encountered the error System.SObjectException: Illegal assignment from String to Datetime and I can't find what is the reason for this error.

The context is as follows. I have a class with the @RestResource annotation and part of its logic is shown below:

I use the following code snippet to maintain a relationship between the fields sent in the JSON and the field it references in Salesforce.

private static Map<String, Schema.SObjectField> notNullabelFields = new Map < String, Schema.SObjectField> {
    'origin' => Account.Origin__c,
    'webId' => Account.WebId__c,
    'documentId' => Account.DocumentId__c,
    'phone' => Account.Phone,
    'language' => Account.Language__c,
    'email' => Account.PersonEmail,
    'prefContactStart' => Account.PreferedContactStart__c,
};

I maintain a wrapper class where I deserialize the JSON request I receive.

 private class RequestData
 {
     public String origin {
         get;
         set {origin = value; setReceivedFields('origin');}}
     public String webId {
         get;
         set {webId = value; setReceivedFields('webId');}}
     public String documentID {
         get;
         set {documentId = value; setReceivedFields('documentID');}}
     public String phone {
         get;
         set {phone = value; setReceivedFields('phone');}}
     public String language {
         get;
         set {language = value; setReceivedFields('language');}}
     public String email {
         get;
         set {email = value; setReceivedFields('email');}}
     public Datetime prefContactStart {
         get;
         set {prefContactStart = value; setReceivedFields('prefContactStart');}}

     private Object get(String property)
        {
            String jsonFormatRequestDataInstance = Json.serialize(this);
            Map<String, Object> untypedRequestDataInstance = (Map<String, Object>) JSON.deserializeUntyped(jsonFormatRequestDataInstance);

            return untypedRequestDataInstance.get(property);
        }
}

The get method of the wrapper class RequestData is used to dynamically obtain the value of the property.

When the WS is called, the request is deserialized into an object of the wrapper class type.

RequestData reqData = (RequestData) JSON.deserializeStrict(request.requestBody.toString(), RequestData.class);

The problem is happening in the code snippet I attach below. What I am trying to do is to dynamically map the values received in the JSON into the appropriate fields in a Salesforce record, as long as these values are not null.

Account acc = new Account();

for (String field : notNullabelFields.keySet())
{
    if (reqData.get(field) != null)
    {
        acc.put(notNullabelFields.get(field), reqData.get(field));
    }
}

When I send the request from Postman the following error appears:

System.SObjectException: Illegal assignment from String to Datetime

I don't understand the reason.

  • Deserialization in the wrapper class is correctly performed (checked)
  • The get method of the wrapper class RequestData returns a value of type Object.
  • The second parameter expected by the put method of the SObject class is of type Object.

At what point is the conversion from Object to String done?


I have tested 'manually' mapping the value of the JSON field prefContactStart to the Salesforce field Account.PreferredContactStart__c and no exception is thrown. In fact, the field in Salesforce is updated correctly.

acc.PreferedContactStart__c = reqData.prefContactStart;

Edit:

Attached is an example of the JSON I receive;

{
  "origin":"origin example",
  "webId":"1234",
  "documentId": "1111111",
  "phone":"+34666666666",
  "language": "German",
  "email": "[email protected]",
  "prefContactStart": "2023-06-26T11:00:00.000Z"
}
2
  • What does your date/time value look like in the JSON? Is it an ISO string? You likely need to convert whatever you receive into an actual Datetime explicitly.
    – Phil W
    Commented Aug 30, 2023 at 13:39
  • Yes, I receive an ISO string. For the field prefContactStart I receive something like that 2023-06-26T11:00:00.000Z. I will update my initial message to attach an example of the JSON I am receiving.
    – Tzinm
    Commented Aug 31, 2023 at 7:12

1 Answer 1

1

JSON only has the following types: Map/Object, Array, String, Number, Boolean and null. it does not have a way to express date-time values, so by convention, we use ISO-8601. The round-trip between your wrapper and an untyped deserialization results in the date time becoming a string. You'd have to reparse the date time back into a DateTime. That's quite a bit of extra work to apparently save a few lines of code. Shorter isn't always better. Skip the unnecessarily expensive JSON serialization/deserialization and just write lines of code explicitly for the detection and assignment of the values.

Edit:

If I deserialize into my wrapper class, why doesn't the direct acc.PreferedContactStart__c = reqData.prefContactStart assignment throw any error? Maybe you are suggesting that the problem is in the get method from my wrapper class where I serialize and deserialize?

The acc.PreferedContactStart__c = reqData.prefContactStart is assigning a DateTime to a DateTime. This is perfectly fine. The types are compatible (they are, after all, the same type), so the assignment is allowed.

The problem lies here:

String jsonFormatRequestDataInstance = Json.serialize(this);
Map<String, Object> untypedRequestDataInstance = (Map<String, Object>) JSON.deserializeUntyped(jsonFormatRequestDataInstance);

In the first line, you take your object and convert it to a JSON string. This is the JSON which you claimed you received, which matches what I'd expect:

{
  "origin":"origin example",
  "webId":"1234",
  "documentId": "1111111",
  "phone":"+34666666666",
  "language": "German",
  "email": "[email protected]",
  "prefContactStart": "2023-06-26T11:00:00.000Z"
}

Now, in the second line, you're using JSON.deserializeUntyped. JSON only has six types, as discussed in the first paragraph. That means that JSON.deserializeUntyped can only ever deserialize to one of those six types. This method doesn't detect, for example, if it's an ISO-8601-formatted string, and it doesn't know how to deserialize an object into a class like RequestData; anything that looks like an Object comes out as a Map.

As a more concrete example, consider:

Object value1 = JSON.deserialize('"2023-08-31T12:34:56.000Z"', DateTime.class);
Object value2 = JSON.deserializeUntyped('"2023-08-31T12:34:56.000Z"');
Assert.isTrue(value1 instanceOf DateTime);
Assert.isTrue(value2 instanceOf String);

As you can see, JSON.deserialize knows the type (we've provided it), and it returns a parsed DateTime object. However, JSON.deserializeUntyped doesn't know about the DateTime object, and so it returns the value as a String.

In other words, your JSON would be represented by the following Apex code:

Map<String, Object> results = new Map<String, Object> {
  'origin'=>'origin example',
  'webId'=>'1234',
  'documentId'=> '1111111',
  'phone'=>'+34666666666',
  'language'=> 'German',
  'email'=> '[email protected]',
  'prefContactStart'=> '2023-06-26T11:00:00.000Z'
};

Notice that prefContactStart is a String, not a DateTime. This is the source of the error. If we substitute the variable's values into the line where the exception is thrown, we get:

acc.put(Account.PreferedContactStart__c, '2023-06-26T11:00:00.000Z');

Which is functionally equivalent to:

acc.PreferedContactStart__c = '2023-06-26T11:00:00.000Z';

As you can see, that won't work. You first need to detect the field's type, then parse if appropriate.

As I said originally, sometimes more code is better. You have to create more work to properly deal with the data, instead of just using the wrapper data directly. To properly support this kind of design, you'd have to write something like my example Xml2JSON class. You'll see in that example that we use type sniffing with some regular expressions in an attempt to do the right thing. This is far more code than if you'd just use the wrapper data directly.

However, if you really wanted to make this work, you could use a backing variable for your wrapper. Here's how I'd probably do that:

class RequestData {
  Map<String, Object> values {
    get {
      if(values == null) {
        values = new Map<String, Object>();
      }
      return values;
    }
    set;
  }
  public String origin {
    get { return (String) values.get('origin'); }
    set { values.put('origin', value); }
  }
  public String webId {
    get { return (String) values.get('webId'); }
    set { values.put('webId', value); }
  }
  public String documentID {
    get { return (String) values.get('documentID'); }
    set { values.put('documentID', value); }
  }
  public String phone {
    get { return (String) values.get('phone'); }
    set { values.put('phone', value); }
  }
  public String language {
    get { return (String) values.get('language'); }
    set { values.put('language', value); }
  }
  public String email {
    get { return (String) values.get('email'); }
    set { values.put('email', value); }
  }
  public Datetime prefContactStart {
    get { return (Datetime) values.get('prefContactStart'); }
    set { values.put('prefContactStart', value); }
  }
  public Set<String> getReceivedFields() {
    return values.keySet().clone();
  }
  public Object getValue(String property) {
    return values.get(property);
  }
}

Now, all the values are preserved in a map cleverly called values, and each getter and setter reads to and writes from this map. Each field that is set will be in the keySet, which I demonstrate by way of getReceivedFields, and you can dynamically get any property with getValue.

If you don't ever intend to serialize to JSON and/or write to any of the fields, you can omit the getters, resulting in a clean:

class RequestData {
  Map<String, Object> values {
    get {
      if(values == null) {
        values = new Map<String, Object>();
      }
      return values;
    }
    set;
  }
  public String origin { set { values.put('origin', value); } }
  public String webId { set { values.put('webId', value); } }
  public String documentID { set { values.put('documentID', value); } }
  public String phone { set { values.put('phone', value); } }
  public String language { set { values.put('language', value); } }
  public String email { set { values.put('email', value); } }
  public Datetime prefContactStart { set { values.put('prefContactStart', value); } }
  public Set<String> getReceivedFields() {
    return values.keySet().clone();
  }
  public Object getValue(String property) {
    return values.get(property);
  }
}

This eliminates the ability to read the properties directly, but also cuts the code by nearly half.

2
  • If I deserialize into my wrapper class, why doesn't the direct acc.PreferedContactStart__c = reqData.prefContactStart assignment throw any error? Maybe you are suggesting that the problem is in the get method from my wrapper class where I serialize and deserialize?
    – Tzinm
    Commented Aug 31, 2023 at 7:29
  • @Tzinm I performed a massive edit that goes into far more detail about the problem and potential solutions. In short, yes, the problem is the roundtrip serialization/deserialization.
    – sfdcfox
    Commented Aug 31, 2023 at 10:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .