6

I'm trying to serialize, then deserialize an AccountHistory record. However, whenever I include a read-only field, such as NewValue, in the JSON, when I try to deserialize the record, I get this error:

System.UnexpectedException: Salesforce System Error: 158360809-105080 (2033965611) (2033965611)

Heres the code I'm working with:

AccountHistory history = [SELECT Id FROM AccountHistory WHERE Id = '017c000009RmtOFAAZ' LIMIT 1];  
AccountHistory historyWithValue = [SELECT Id, NewValue FROM AccountHistory WHERE Id = '017c000009RmtOFAAZ' LIMIT 1];

String historyString = JSON.serialize(history);

SObject someObj = (sObject)JSON.deserialize(historyString, sObject.class); // Works 
AccountHistory someHistory = (AccountHistory)JSON.deserialize(historyString, AccountHistory.class); // Works 

String historyValueString = JSON.serialize(historyWithValue);

SObject anotherObj = (sObject)JSON.deserialize(historyValueString, sObject.class); // Fails
AccountHistory anotherHistory = (AccountHistory)JSON.deserialize(historyValueString, AccountHistory.class); // Fails

How can I deserialize JSON data which includes a record with read-only fields?


I know there's a method of using JSON to set the CreatedDate on records for test contexts, which is a read-only field, so there must be some solution. As this answer states:

This doesn't enforce the normal read-only field attributes that prevent you from setting a createdDate value.

However, this does not seem to be the case for other object fields. For example, this code works:

String someJson = '{"AccountHistory":{"CreatedDate":"2017-11-07T15:56:30.000+0000"}}';
AccountHistory history = (AccountHistory)JSON.deserialize(someJson, AccountHistory.class); // Works

But this code causes a Salesforce exception:

String someJson = '{"AccountHistory":{"NewValue":"SomeValue"}}';
AccountHistory history = (AccountHistory)JSON.deserialize(someJson, AccountHistory.class); // Fails

Trying to update either of these fields through a constructor causes this exception:

new AccountHistory(NewValue = DateTime.now(), CreatedDate = DateTime.now());

Field is not writeable: AccountHistory.NewValue

Field is not writeable: AccountHistory.CreatedDate

The error message for both fields is the same, however, the behavior of the fields vary when it comes to JSON methods.


Trying to use Test.LoadData also fails with the same exception type. This was tested using this .csv file:

Field,OldValue,NewValue,AccountId
Name,SomeTest,AnotherTest,001c000001GWSaL

And this snippet in my @testSetup method:

Test.loadData(Schema.SObjectType.AccountHistory.getsObjectType(), 'ActivityHistoryTestData');

System.UnexpectedException: Salesforce System Error: 797046050-85268 (-725019502) (-725019502)


The context behind this question involves creating test AccountHistory records. Since the OldValue and NewValue fields are not writeable, and Field History tracking is disabled in test contexts, I'm left with few options for writing a test class. I've already refactored a number of functions to allow raw data to be passed to them, allowing me to test them without requiring any history records. However, I'd still like to be able to test some other methods to obtain full coverage.

My solution to this problem was to create json test data, and then deserialize the data into history records, or at least sObjects. However, I've run into some trouble with the conversion from a string into the object.

11
  • Have you tried Test.loadData?
    – Adrian Larson
    Nov 8, 2017 at 19:25
  • Sadly that also fails to insert the records. The error provided is also a Salesforce system error, but has a different set of numbers with it. Doesn't seem to matter much, since it fails right out of the gate. Nov 8, 2017 at 20:21
  • Json.deserialize certainly works with read only fields as I use that all the time to mock formula fields and system audit fields; the issue is more with someSobjectHistory records
    – cropredy
    Nov 10, 2017 at 23:43
  • 1
    oh, I see it doesn't depend on the field
    – Patlatus
    Nov 11, 2017 at 13:50
  • 1
    @KiranMachhewar: I have tried that, this doesn't work
    – Patlatus
    Nov 11, 2017 at 16:38

1 Answer 1

6
+50

I think the problem is that JSON uses strict concrete type definition.

You can't write something like

AccountHistory history = (AccountHistory)JSON.deserialize('{"NewValue":a}', AccountHistory.class);

because that would raise exception

System.JSONException: Unexpected character ('a' (code 97)): expected a valid value (number, String, array, object, 'true', 'false' or 'null') at [line:1, column:14]

So this means you can put values for numbers, strings, arrays, objects (maps), booleans or null.

While EntityHistory NewValue field has type "anyType" in SOAP

enter image description here

So when Apex tries to deserialize JSON and put any concrete value to anyType field it fails, since types may not match, the actual type of this anyType field depends on Field column, but Apex translator is unfortunately unable to resolve this reference and detect that Field column value is Name, so string values are correct for NewValue even if you specify value for the Field column:

AccountHistory history = (AccountHistory)JSON.deserialize('{"Field":"Name", "NewValue":"a"}', AccountHistory.class);

System.UnexpectedException: Salesforce System Error: 1492486810-150788 (2033965611) (2033965611)

The only value which satisfies anyType type is null, so when you give null value it doesn't fail:

AccountHistory history = (AccountHistory)JSON.deserialize('{"Field":"Name", "NewValue":null}', AccountHistory.class);

Success

So I see the following options:

  1. Use only null values for NewValue and OldValue fields
  2. Try to use Test.isRunningTest() and mock some dependent logic with it.
  3. Try to use Test.createStub() method and StubProvider interface implementation to mock dependent logic with it.

Mocking framework with Stub API is a wonderful feature which I have never heard before and which I have found about very-very recently. This is ability to mock tests with stub interface

It is now possible to create sophisticated stub interface implementation and use it with method Test.createStub to avoid using of Test.isRunningTest().

=================

Augmenting research results from answers to similar questions.

This answer suggests there to use Test.loadData but it is clear that using using of Test.loadData for AccountHistory object clearly raises internal error exception, for all cases

with NewValue or OldValue

AccountId,Field,OldValue,NewValue
001b0000008g2iPAAQ,Name,a,b
001b0000008g2iQAAQ,Name,c,d

enter image description here

with no NewValue or OldValue column

AccountId,Field
001b0000008g2iPAAQ,Name
001b0000008g2iQAAQ,Name

with empty values of those fields

AccountId,Field,OldValue,NewValue
001b0000008g2iPAAQ,Name
001b0000008g2iQAAQ,Name

In this answer it is suggested to split processing of records into several parts.

  1. Have a DAO method which retrieves the actual records similar to queryDatabase in quoted answer
  2. Have a method which converts records into strings or objects
  3. Have a method which processes strings or objects converted from records.

You can test the first method that it returns zero records in test context. You can test the second method that if you build records without NewValue or OldValue it converts them correctly. You can test the actual logic passing string or object instead of real records.

Also as the last resort you can try to use SeeAllData=true like it is suggested in this answer.

6
  • 1
    This is really interesting - I'll try writing some code with this in mind on Monday and see what happens! Nov 11, 2017 at 18:11
  • @battery.cord I have updated my answer augmenting research results from answers to similar questions. Probably you might try to combine all of these to get it work. So you might use Test.isRunningTest() or Test.createStub() to mock some method or just split your code to three parts: querying records, processing records to models, and actual logic to be performed on models, not records. This way you will gain 100% coverage testing each part separately.
    – Patlatus
    Nov 11, 2017 at 18:42
  • ApexMocks is a framework on top of stub interface and my preferred solution
    – cropredy
    Nov 12, 2017 at 3:57
  • Specifically, the code I need to test is sorting and creating wrapper data from the query result. I think if I switched the parameter for the sorting to another wrapper, so I can provide custom values through a stub or etc, that would let me provide coverage. Stubbing without a wrapper class seems pointless, since I won't be able to create the records at all, and If I'm using a wrapper, I could just create the data in the test. I guess I don't really see a benefit from using a stub in this situation, since I can't create then return history records from the stub. Nov 13, 2017 at 14:13
  • so if you need to test sorting and creating wrapper then it really doesn't make sense to user stub, you could just create wrapper and pass it to the method you are testing. So are you able to get 100% coverage?
    – Patlatus
    Nov 13, 2017 at 16:03

You must log in to answer this question.

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