Home > Mobile >  C# check if deserialized object has specific field
C# check if deserialized object has specific field

Time:10-29

So, I want to save object and then load it and take data from it. I made an class called SaveData, in there I have field isVibrationOn.

Working code below:

public class SaveData
{
    public bool isVibratonOn;
}

Here is the code for serialization:

public void SaveGame()
{
     SaveData saveData = new SaveData();

     BinaryFormatter bf = new BinaryFormatter();

     FileStream file = File.Create(Path.Combine(Application.persistentDataPath, FILE_NAME));

     SaveData(saveData);

     bf.Serialize(file, saveData);
     file.Close();
}

private void SaveData(SaveData saveData)
{
     saveData.isVibrationOn = VibrationController.controller.isVibrationOn;
}

And here is code for load data:

public void LoadGame()
{
        if (File.Exists(Path.Combine(Application.persistentDataPath, FILE_NAME)))
        {
            FileStream file = File.Open(Path.Combine(Application.persistentDataPath, FILE_NAME), FileMode.Open);

            if (file.Length > 0)
            {
                SaveData saveData = new SaveData();
                BinaryFormatter bf = new BinaryFormatter();
            
                saveData = (SaveData)bf.Deserialize(file);

                LoadData(saveData);

                file.Close();
            }
        }
}

public LoadData(SaveData saveData)
{

        VibrationController.controller.isVibrationOn = saveData.isVibrationOn;

}

My question here is, when I decide to add or remove some fields (lists etc) to SaveData object, my LoadData would look different, but object that is saved on device whould have different fields as well. Simple expample

public LoadData(SaveData saveData)
{

        VibrationController.controller.isVibrationOn = saveData.isVibrationOn;

        //old save data doesn't have isMusicOn field
        //LoadData method is different because I added new field on SaveData object after I saved file.
        //This is simple example, but also it could be any changes like list of objects with an object that has different fields added/changed.
        MusicController.controller.isMusicOn = saveData.isMusicOn;

}

How would I check if old instance has that field?

CodePudding user response:

As a general recommendation, do not use binaryFormatter. It is slow, inefficient, unsafe and has poor backwardscompatibility.

So if you change the class I would not expect it to be possible to de serialize older data at all, let alone tell you what fields where missing. Switching .net versions can also be an issue with binaryformatter.

There are much better serialization libraries out there. Json.net is the standard for text-based serialization, and I have used protobuf.net for binary serialization. But there are many other libraries that can be used.

To handle missing or optional fields you would typically have some default value, like null, that you can check. It should also be possible to initialize the fields to some other default value if desired.

I would recommend separating your serialization objects from your domain objects, since serialization frameworks may require parameter less constructors or public setters. And separate serialization objects provide a chance to manage differences in object structures between versions.

CodePudding user response:

If your goal is to make sure your code doesn't break because of "missing" field:

If you already have the "old" version rolled out, that's tough - you will have to implement some kind of "migration" from old data to new, probably by keeping the old class as-is, implementing your changes in a new class (possibly derived from old, to keep code duplication to a minimum), and then checking if the data you are deserializing is old (I'll refer to it as "MyClass_Old") or new ("MyClass_New"). If you are able to determine that from some metadata, file attributes or such - great. If not, you could just deserialize it as MyClass_New on purpose and wrap in in try-catch. If you caught a SerializationException, then it's probably MyClass_Old, and then you deserialize it as MyClass_Old and then use it to construct a new MyClass_New instance.

However, if you didn't yet roll out those changes, you can make use of version-tolerant serialization. You can use attributes like [OptionalFieldAttribute] to mark fields that might be missing in different version, [OnDeserializingAttribute] (that goes on method that will be called before deserializing - to maybe set some values in those missing fields), and [OnDeserializedAttribute] to "fix" or validate your deserialized object.

So for your example, knowing that you added the isMusicOn field, you'd mark it as optional (because it might be missing in deserialized data):

[OptionalField(VersionAdded = 2)]
bool isMusicOn;

and then set it to some kind of default value if it's missing. Let's say you want it to be on by default:

[OnDeserializing]
internal void OnDeserializingMethod(StreamingContext context)
{
    isMusicOn = true;
}

Note that here you use the "deserializing" attribute, because you do want to keep the value if it is not missing. Since OnDeserializingMethod is called before deserializing, it will be overwritten by deserialized value if it is present. This specific case (you have a field that might be missing, and you want it to have some specific value) is also covered here.

If your goal is to check if this specific field was missing in deserialized object:

With some preparation this same principle (version-tolerant serialization) can also be used to specifically determine if the field was present in deserialized object. You can set it to some predetermined value (one that would not be allowed when serializing) before deserializing (using [OnDeserializing]). Then after deserialization check if that value is still there or it was replaced by something reasonable.

So in your case let's say that allowed values for isMusicOn are true and false, then you can make your isMusicOn a nullable bool:

[OptionalField(VersionAdded = 2)]
bool? isMusicOn;

Then you'll make sure that serialized object cannot have isMusicOn set to null, by using [OnSerializing]:

[OnSerializing()]
internal void OnSerializingMethod(StreamingContext context)
{
    if (isMusicOn == null) 
        isMusicOn = false;
}

Then assign null in a method marked with [OnDeserializing]:

[OnDeserializing]
internal void OnDeserializingMethod(StreamingContext context)
{
    isMusicOn = null;
}

and then in [OnDeserialized] check if it is still null (if it was deserialized, it will have changed to proper value like true or false, if not it'll stay null):

[OnDeserialized]
internal void OnDeserializedMethod(StreamingContext context)
{
    Console.WriteLine($"isMusicOn {isMusicOn == null ? "wasn't" : "was" } present in deserialized object!");
    if (isMusicOn == null)
        isMusicOn = false;//set it to some "proper" default value;
}

In this example, actually, nullable value wouldn't even require the setup (it'll be null by default even without the OnDeserializing part), but i'll leave it in as an example.

All this last part is probably more trouble then it's worth. Besides all the hassle with attributes, you have to change your field type to allow for that "super special value", and that might require you to change a lot of other code that depends on it. I suspect the real question is not "how to check if the field was there", but rather "how to make sure my app doesn't break because it wasn't there", and for that you don't need to know if your field was deserialized - just to make sure that it has some reasonable value in it after deserialization.

  • Related