ISerializationCallbackReceiver methods called without trigger

Hello! So Im having next object and I want to have initialized DateTime field after deserialization with JsonUtility.FromJson();

[Serializable]
public class LastActivatedDailyRewardsData : ISerializationCallbackReceiver
{
    public int lastActivatedRewardIndex;
    public string activationTimeString;
    public DateTime activationTime {get; private set;}

    public LastActivatedDailyRewardsData(int lastActivatedRewardIndex, DateTime activationTime) {
        this.lastActivatedRewardIndex = lastActivatedRewardIndex;
        this.activationTime = activationTime;
        activationTimeString = TimeToString(activationTime);
    }

    string TimeToString(DateTime activationTime) {
        return activationTime.ToString("u");
    }

    public void OnBeforeSerialize(){}

    public void OnAfterDeserialize()
    {  
        activationTime = DateTime.Parse(activationTimeString).ToUniversalTime();
    }

It works ok, but for some reason this ‘OnAfterDeserialize’ method is called with game start even if I didnt try to deserialize anything. Log message in this method shows that int and string fields of this mistery object are 0 and “” so it fails with format exception.

So is it ok for this interface to call some mistery empty object or Im using this interface in wrong way?

Assets are reserialised more often than you would expect. Particularly during domain reloads, which happens when entering play mode. The behaviour here is expected.

That said, I would not be using the interface if you don’t intend to use both callbacks. The intent is to convert data that can’t be serialised into some that can in OnBeforeSerialize, and then convert said serializable data back again in OnAfterDeserialize. Doing only one of the two defeats its purpose.

If you only want it to happen once, then have your property lazily initialize an underlying value when its accessed for the first time. I often use this pattern as ISerializationCallbackReceiver can get fairly heavy in complicated inspectors as assets are reserialised every repaint in any inspector with IMGUI.

To clarify:

Whenever you have used “LastActivatedDailyRewardsData” somewhere in a serialized runtime class, it may be serialized / deserialized, even when it’s not part of an explictily serialized object. The reason is that the editor will also serialize / deserialize private variables which are not marked with SerializeField during domain reloads. You can prevent this by marking private variables with NonSerialized. This will exclude this variable from serialization completely. Of course that means that during hot-reloading the instance would be lost.

ps: Do you need the activation time as a string because it may be read by some external API? If you just want to serialize it so it can be loaded back, it’s more compact and faster to store the Ticks of the DateTime as a single long value. Ticks are in 100 nano-second intervals since midnight of 0001.01.01. It may need to be combined with ToUniversalTime if it’s important to be universal, otherwise it would be local time. Though this all depends on your exact usecase.

Thank you for advice!

To clarify I understood ‘lazy initialization’ correctly - is smth simple like next property is enough?

 private DateTime _activationTime;
     public DateTime activationTime2 {get {
         if (_activationTime == null) {
             _activationTime = timeToDateTime(activationTimeString);
         }
         return _activationTime;
     } private set {
         _activationTime = value;
     }}

Or I need to use smth fancy like this(the official lazy initialization I just googled)?

      public Lazy<DateTime> activationTime;
 
     public LastActivatedDailyRewardsData(int lastActivatedRewardIndex, DateTime activationTime) {
         this.lastActivatedRewardIndex = lastActivatedRewardIndex;
         this.activationTime = new Lazy<DateTime>(() => activationTime);
         activationTimeString = TimeToString(activationTime);
     }
 
     public LastActivatedDailyRewardsData() {
         activationTime  = new Lazy<DateTime>(() => timeToDateTime(activationTimeString));
     }

Whenever you have used “LastActivatedDailyRewardsData ” somewhere in a serialized runtime class, it

By this you mean ‘used LastActivatedDailyRewardsData in another class marked as [Serializable] even not as property to serialize to?’ If yes - my answer is no, Im using it only to handy work with values stored in json format in properties. So its only mentioned In ‘poperty manager’ and some ‘mono behaviour’ classes. In my previous researches I found some info about objects marked as [Serializable] actually having ISerializationCallbackReceiver interface. So is it possible my problem is caused of using [Serializable] and ISerializationCallbackReceiver at the same class?

ps-answer: yes, Im using date as string only to store it in properties for local usage, thank you I will start to store date as ‘long’ of ticks from this day)

Like the first method, though DateTime is a struct so it won’t ever be null. You wouldn’t need to check it isn’t something like DateTime.MinValue, or potentially use a nullable value type, ergo DateTime?.

What do you mean by “mentioned” in mono behaviour classes? MonoBehaviours are always serialized and it includes all fields. So are there any field of that type, somewhere? Since you put a Debug.Log in your callback, what does the callstack look like? It should give some hints where it’s actually deserialized.

Unity does not call any of those callbacks on random classes. Unity itself only serializes MonoBehaviour / ScriptableObject classes (and any data that is nested in those) or when you use the JsonUtility on a Serializable class. One of those cases must be the reason.

Yes, there is private field of LastActivatedDailyRewardsData type in monobehaviour script (intialized in ‘Awake’, used and updated by new objects in some methods)

But the problem is Im having double call of ‘OnAfterDeserialize’ method just after I pressed ‘Play’ button in editor. But only one deserialization is expected before you start make some changes in the game. And if I comment [Serializable] tag in LastActivatedDailyRewardsData class, leaving it only with ISerializationCallbackReceiver interface I stop recieving this unexpected double calls

Log callback for unexpected call looks pretty empty:

OnAfterDeserialize CALL index=0, time=
UnityEngine.Debug:Log (object)
LastActivatedDailyRewardsData:OnAfterDeserialize () (at Assets/Scripts/Utils/LastActivatedDailyRewardsData.cs:39)

while expected call callback looks like this

OnAfterDeserialize CALL index=0, time=2024-11-19 10:52:28Z
UnityEngine.Debug:Log (object)
LastActivatedDailyRewardsData:OnAfterDeserialize () (at Assets/Scripts/Utils/LastActivatedDailyRewardsData.cs:39)
UnityEngine.JsonUtility:FromJson (string)
PlayerPrefsManager:GetDailyRewardsStatus () (at Assets/Scripts/Utils/PlayerPrefsManager.cs:355)
DailyRewardProgressbarBehaviour:Awake () (at Assets/Scripts/EffectsShop/DailyRewardProgressbarBehaviour.cs:24)

Have you tried putting System.NonSerialized on that private field? This would prevent Unity from serializing (and I guess it also should prevent deserializing) this field at all. This behaviour is mentioned here.

Unity restores them to their original, pre-serialization values:

  • Unity restores all variables - including private variables - that fulfill the requirements for serialization, even if a variable has no [SerializeField] attribute. Sometimes, you need to prevent Unity from restoring private variables, for example, if you want a reference to be null after reloading from scripts. In this case, use the [field: NonSerialized] attribute.
  • Unity never restores static variables, so don’t use static variables for states that you need to keep after Unity reloads a script because the reloading process will discard them.

Yes, you are right, I added [NonSerialized] to my field in monobehaviour and the unexpected deserialization method calls are gone, while the LastActivatedDailyRewardsData class still has [Serializable] and ISerializationCallbackReceiver at the same time.

Thanks for the repeated and patient explanations, didn’t get the idea from the first time)