Config JsonSerializerSettings for IPlayerDataService.SaveAsync()

Hi,
I have some complex objects that need to be serialized into JSON using JsonSerializerSettings like this:

I want to save them in the cloud as JSON.
Obviously, I can first serialize them first and then pass them into a Dictionary<string, object>, which can be used as a parameter in IPlayerDataService.SaveAsync().
However, the JSON displayed on the cloud dashboard contains multiple backslashes (due to re-serialization within the SDK), making it very difficult to view and manipulate manually.
9784578--1403661--upload_2024-4-22_0-6-47.png

So, my question is: Is there any way I can customize the JsonSerializerSettings of the CloudSaveSystem? And then simply put my objects into the Dictionary<string, object>? Or, perhaps a better method would be to ignore the SDK’s serialization when the object is a string?

Sorry if any part wasn’t clear enough. I’ll provide more information if needed.

Thank you!

Try adding a simple string with quotes. These will likely also be escaped to \“ and if that is the case, there is probably nothing you can do about this.

However, you can edit such a json in any decent IDE which will allow you to convert a json string from and to an escaped string. So you could copy the json, paste it, unescape it, edit it, escape it, copy and paste into web.

Although honestly he escape characters shouldn‘t bother you because you will want to edit only the values themselves, not the structure of the json.

The better alternative however may be to split your complex object into individual variables. It may be less convenient in code but if you often need to edit the contents in the web interface it may be worth the effort.

1 Like

We see folks trip on how best to serialise data, I think in part because it’s not as well covered in the documentation as it could be and because this behaviour was confusing in earlier versions of Cloud Save.

The sample that comes with the SDK has good examples of how to load and save objects that includes appropriate error handling, but for quick reference (without any error handling, just to focus on serialisation!) folks might find this example code helpful:

Example data structure:

using System;
using System.Collections.Generic;

[Serializable]
public class SampleObject
{
  public string Name;
  public List<SampleItem> Items = new List<SampleItem>();
}

public class SampleItem
{
  public string ItemName;
  public int ItemInt;
  public float ItemFloat;
  public bool ItemBool;
}

Example of how to save and load data from Cloud Save:

using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Unity.Services.Authentication;
using Unity.Services.CloudSave;
using Unity.Services.CloudSave.Models;
using Unity.Services.Core;
using UnityEngine;

public class CloudSaveObjectExample: MonoBehaviour
{
  private async void Awake()
  {
    // Sign in with Unity Authentication
    await UnityServices.InitializeAsync();
    await AuthenticationService.Instance.SignInAnonymouslyAsync();

    // Create a new sample object
    SampleObject newSampleObject = new SampleObject { Name = "My sample object" };
    newSampleObject.Items.Add(
      new SampleItem
     {
        ItemName = "Item 1",
        ItemInt = 1,
        ItemFloat = 3.14f,
        ItemBool = true
     }
   );

    // Save the object to Cloud Save Player Data, get a write lock back
    string writeLock = await SaveSampleObject("myKey", newSampleObject);
    Debug.Log($"Saved {JsonConvert.SerializeObject(newSampleObject)} and got write lock {writeLock}");

    // Load sample object from Cloud Save Player Data
    SampleObject sampleObject = await LoadSampleObject<SampleObject>("myKey");
    Debug.Log($"Loaded {JsonConvert.SerializeObject(sampleObject)}");

    // Modify the item we just got back from Cloud Save
    sampleObject.Items.Add(
        new SampleItem
        {
          ItemName = "Item 2",
          ItemInt = 2,
          ItemFloat = 4.28f,
          ItemBool = false
        }
    );

    // Save the modified item back to Cloud Save
    writeLock = await SaveSampleObject("myKey", sampleObject, writeLock);
    Debug.Log($"Saved {JsonConvert.SerializeObject(sampleObject)} and got new write lock {writeLock}");
  }

  private async Task<string> SaveSampleObject(string key, object value, string writeLock = null)
  {
    Dictionary<string, string> result = await CloudSaveService.Instance.Data.Player.SaveAsync(
      new Dictionary<string, SaveItem> { { key, new SaveItem(value, writeLock) } }
    );
    return result[key]; // return write lock
  }

  private async Task<T> LoadSampleObject<T>(string key)
  {
    var results = await CloudSaveService.Instance.Data.Player.LoadAsync(new HashSet<string> { key });

    if (results.TryGetValue(key, out var item))
    {
      return item.Value.GetAs<T>();
    }
    else
    {
      return default;
    }
  }

}

Essentially, you don’t need to serialise objects first, the Cloud Save SDK will do that for you.

In this example, it will look like this in the Unity Dashboard (i.e. strings won’t be double-escaped):


Soon (possibly later this week) you will be able to edit Player Data from the Dashboard too - a feature we already rolled out for Custom Items used for Game Data.

3 Likes

Will SaveItem also serialize the List Items directly?
I’m asking because JsonUtility won’t.

1 Like

Great question and yes, it should “just work”!

The screenshot is what you should see in the Dashboard, and this is what should be displayed in the console:

9834480--1414662--upload_2024-5-14_15-22-30.png

Of course behaviour across versions of a project might get tricky if the data models for a game change a lot. We don’t have any guidance on the best ways handle that, but it’s something we are mindful of as a gap.

2 Likes

Hi, thanks for the responses! However, I think you’ve misunderstood my question. Here are the JSON outputs produced by:

  • A (using JsonConvert.SerializeObject with my custom JsonSerializerSettings):
{
    "CategoryDatas": {
        "currency": {
            "$type": "Game.Feature.Inventory.Data.CurrencyCategoryData, Assembly-CSharp",
            "CategoryId": "currency",
            "CategoryItems": [
                {
                    "$type": "Game.Feature.Inventory.Data.CurrencyItemData, Assembly-CSharp",
                    "ItemId": "currency.gold",
                    "Quantity": {
                        "Value": 0
                    }
                },
                {
                    "$type": "Game.Feature.Inventory.Data.CurrencyItemData, Assembly-CSharp",
                    "ItemId": "currency.golden-fish",
                    "Quantity": {
                        "Value": 42
                    }
                },
                {
                    "$type": "Game.Feature.Inventory.Data.CurrencyItemData, Assembly-CSharp",
                    "ItemId": "currency.pearl",
                    "Quantity": {
                        "Value": 8
                    }
                }
            ]
        },
        "expirable": {
            "$type": "Game.Feature.Inventory.Data.ExpirableCategoryData, Assembly-CSharp",
            "CategoryId": "expirable",
            "CategoryItems": [
                {
                    "$type": "Game.Feature.Inventory.Data.ExpirableItemData, Assembly-CSharp",
                    "ItemId": "expirable.vip",
                    "Quantity": {
                        "Value": 0
                    },
                    "StartedTime": 0
                }
            ]
        }
    }
}

-B (using CloudSaveSDK default JsonSerializerSettings):

{
    "CategoryDatas": {
        "currency": {
            "CategoryId": "currency",
            "CategoryItems": [
                {
                    "ItemId": "currency.gold",
                    "Quantity": {
                        "Value": 0
                    }
                },
                {
                    "ItemId": "currency.golden-fish",
                    "Quantity": {
                        "Value": 0
                    }
                },
                {
                    "ItemId": "currency.pearl",
                    "Quantity": {
                        "Value": 0
                    }
                }
            ]
        },
        "expirable": {
            "CategoryId": "expirable",
            "CategoryItems": [
                {
                    "ItemId": "expirable.vip",
                    "Quantity": {
                        "Value": 0
                    },
                    "StartedTime": 0
                }
            ]
        }
    }
}

Here is also the code snippet to generate the above JSONs

As you can see, the result from B is missing the $type attribute (and there are actually more cases like this from another data). This is why I have a function ConvertToDictionary that first serializes my object into a JSON string and then puts it into a Dictionary<string, object>.

So, let’s go back to my question:

  • Is there any way I can customize the JsonSerializerSettings of the CloudSaveSystem?
    And then simply put my object into the Dictionary<string, object>? Or perhaps, a better method would be to ignore the SDK’s serialization when the object is a string?
    Sorry if I have any misunderstanding here! I’ll provide more information if any part wasn’t clear enough.
    Thank you!

Here is also the class structure of the data that I use for serialization:

Three classes inherit from the ICategoryData interface, and two classes inherit from the IItemData interface. Due to the complexity of my data structure, I need custom JsonSerializerSettings.

If you need to transform the object from another data structure before saving then you can have a function that converts your data structure but which keeps it as an object - i.e. without also serialising as a string.

For example, if we take JSON object serialised to a string above, then load it as a JSON object and pass that object through to Cloud Save with the code below, it is then saved as an object as you might expect and is readable and nicely formatted in the Dashboard.

string json = File.ReadAllText(@"/tmp/test-object.json");
JObject testObject = JObject.Parse(json);
Dictionary<string, string> result = await CloudSaveService.Instance.Data.Player.SaveAsync(
    new Dictionary<string, object> { { "testObject", new SaveItem(testObject, null) } }
 );

If the approach you are using can’t do that and can only serialise to a string, then you could add a call like JObject.Parse(string); to convert the string back into an object before saving it.

1 Like

What a great solution, why haven’t I thought of this before.
Thank you.

1 Like