Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Bug Jsons not being deserialized properly for build, only for editor.

Discussion in 'Scripting' started by Hunter2e, Aug 29, 2023.

  1. Hunter2e

    Hunter2e

    Joined:
    Feb 24, 2023
    Posts:
    3
    The following code works perfectly fine for the editor and allows me to save and load an item database. But when I build I'm immediately faced with a null reference exception pertaining to the line:

    Debug.Log(wrapper.items[0].Id);


    It seems LoadData() prints the contents of the Json => Debug.Log(jsonToLoad);
    but directly after this line when trying to access the first items name an error is thrown as the fields of the items don't actually seem to have been set. Again, this problem only happens when I build. Any help is appreciated. The issue is likely within the LoadData() or SaveData() functions.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using Newtonsoft.Json;
    5. using Newtonsoft.Json.Linq;
    6. public static class JsonDataManager {
    7.     private static string SaveFilePath = Application.streamingAssetsPath + "/data.json";
    8.     private static string RecipeSaveFilePath = Application.streamingAssetsPath + "/recipes.json";
    9.  
    10.  
    11.     public static void AddBaseItemTemplateToJson(BaseItemTemplate baseItemTemplate) {
    12.         // Load existing data from JSON
    13.         List<BaseItemTemplate> existingData = LoadData();
    14.  
    15.         // Add the new baseItemTemplate to the list
    16.         existingData.Add(baseItemTemplate);
    17.  
    18.         // Save the updated data to JSON
    19.         SaveData(existingData);
    20.     }
    21.  
    22.     public static void SaveData(List<BaseItemTemplate> data) {
    23.         /*
    24.         ItemTemplateWrapper wrapper = new ItemTemplateWrapper(data);
    25.         string jsonToSave = JsonConvert.SerializeObject(wrapper, Formatting.Indented,
    26. new JsonSerializerSettings {
    27.     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    28. });*/
    29.         ItemTemplateWrapper wrapper = new ItemTemplateWrapper(data);
    30.         string jsonToSave = JsonUtility.ToJson(wrapper);
    31.  
    32.         File.WriteAllText(SaveFilePath, jsonToSave);
    33.     }
    34.  
    35.     public static List<BaseItemTemplate> LoadData() {
    36.         Debug.Log(SaveFilePath);
    37.             string jsonToLoad = File.ReadAllText(SaveFilePath);
    38.  
    39.         //ItemTemplateWrapper wrapper = JsonConvert.DeserializeObject<ItemTemplateWrapper>(jsonToLoad);
    40.         Debug.Log(jsonToLoad);
    41.             ItemTemplateWrapper wrapper = JsonUtility.FromJson<ItemTemplateWrapper>(jsonToLoad);
    42.         Debug.Log(wrapper.items[0].Id);
    43.             foreach(BaseItemTemplate item in wrapper.items) {
    44.             try {
    45.                 item.prefab = Resources.Load<GameObject>(item.prefabString);
    46.                 item.icon = Resources.Load<Sprite>(item.slug);
    47.             } catch {
    48.  
    49.             }
    50.            
    51.         }
    52.             return wrapper.items;
    53.     }
    54.  
    55.     public static List<RecipeTemplate> LoadRecipeData() {
    56.  
    57.             string jsonToLoad = File.ReadAllText(RecipeSaveFilePath);
    58.        
    59.             RecipeTemplateWrapper wrapper = JsonUtility.FromJson<RecipeTemplateWrapper>(jsonToLoad);
    60.             return wrapper.recipes;
    61.     }
    62.  
    63.     public static void SaveRecipeData(List<RecipeTemplate> recipes) {
    64.         RecipeTemplateWrapper wrapper = new RecipeTemplateWrapper(recipes);
    65.         string jsonToSave = JsonUtility.ToJson(wrapper);
    66.         File.WriteAllText(RecipeSaveFilePath, jsonToSave);
    67.     }
    68. }
    69.  
    70. [System.Serializable]
    71. public class ItemTemplateWrapper {
    72.     public List<BaseItemTemplate> items;
    73.  
    74.     public ItemTemplateWrapper(List<BaseItemTemplate> itemList) {
    75.         items = itemList;
    76.     }
    77. }
    78.  
    79. [System.Serializable]
    80. public class RecipeTemplateWrapper {
    81.     public List<RecipeTemplate> recipes;
    82.  
    83.     public RecipeTemplateWrapper(List<RecipeTemplate> recipeList) {
    84.         recipes = recipeList;
    85.     }
    86. }
    87.  
     

    Attached Files:

  2. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    First of all your error screenshot is talking about like 48 in ItemDatabase.cs inside a method called CreateItemDatabase. So how do you know the error originates from the code you mentioned?

    Apart from that both of your wrapper classes do not have a parameterless constructor. Most serializers requires a parameterless constructor as serialization systems can not use custom constructors as they have no idea what they should pass to it. Some serializers get around that by using low-level object initialization methods that does not call a constructor at all. However depending on the platform this behaviour may differ. So I would highly recommend when you implement a parameterized constructor, add a parameterless constructor as well
     
    Hunter2e likes this.
  3. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,561
    You are not generally going to be able to write to the streaming assets path. Review the docs for more details. The streaming assets system is for you to send unimported raw data to the user, data which you can then read out of there for runtime use.

    You want Application.persistentDataPath for saving user data.

    HOWEVER, you need to not call that at static constructor time. Unity might not be up and ready and have decided where that data is supposed to go on all platforms. Make it a lazy getter or at least do it AFTER Unity starts running. Static ctors happen very early in the lifecycle, I think when the DLLS first mount, which might even be long before Unity fully starts up.

    Let me drop a fat load of juicy knowledges on you to save you wasting a lot of time. See below. Save systems are extremely well understood while at the same time finicky and tough to get fully implemented.

    Definitely do the process one tiny step at a time, not all at once, or you will be very sad. Like start by saving a single integer, for instance.

    Load/Save steps:

    https://forum.unity.com/threads/save-system-questions.930366/#post-6087384

    An excellent discussion of loading/saving in Unity3D by Xarbrough:

    https://forum.unity.com/threads/save-system.1232301/#post-7872586

    Loading/Saving ScriptableObjects by a proxy identifier such as name:

    https://forum.unity.com/threads/use...lds-in-editor-and-build.1327059/#post-8394573

    When loading, you can never re-create a MonoBehaviour or ScriptableObject instance directly from JSON. The reason is they are hybrid C# and native engine objects, and when the JSON package calls
    new
    to make one, it cannot make the native engine portion of the object.

    Instead you must first create the MonoBehaviour using AddComponent<T>() on a GameObject instance, or use ScriptableObject.CreateInstance<T>() to make your SO, then use the appropriate JSON "populate object" call to fill in its public fields.

    If you want to use PlayerPrefs to save your game, it's always better to use a JSON-based wrapper such as this one I forked from a fellow named Brett M Johnson on github:

    https://gist.github.com/kurtdekker/7db0500da01c3eb2a7ac8040198ce7f6

    Do not use the binary formatter/serializer: it is insecure, it cannot be made secure, and it makes debugging very difficult, plus it actually will NOT prevent people from modifying your save data on their computers.

    https://docs.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide

    Problems with Unity "tiny lite" built-in JSON:

    In general I highly suggest staying away from Unity's JSON "tiny lite" package. It's really not very capable at all and will silently fail on very common data structures, such as bare arrays, tuples, Dictionaries and Hashes and ALL properties.

    Instead grab Newtonsoft JSON .NET off the asset store for free, or else install it from the Unity Package Manager (Window -> Package Manager).

    https://assetstore.unity.com/packages/tools/input-management/json-net-for-unity-11347

    Also, always be sure to leverage sites like:

    https://jsonlint.com
    https://json2csharp.com
    https://csharp2json.io
     
    Hunter2e likes this.
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    The simplest solution to this is just make them properties (aka methods):
    Code (CSharp):
    1. private static string SaveFilePath => Application.persistentDataPath + "/data.json";
    2.  
    3. private static string RecipeSaveFilePath => Application.persistentDataPath + "/recipes.json";
     
    h1ggs, Hunter2e and Kurt-Dekker like this.
  5. Hunter2e

    Hunter2e

    Joined:
    Feb 24, 2023
    Posts:
    3
    That is where the items begin getting assigned by the data which was read so back tracking gets me to LoadData() which is the root of the error.


    Code (CSharp):
    1. private static List<Item> database = new List<Item>();
    2.     private static List<BaseItemTemplate> itemList = new List<BaseItemTemplate>();
    3.  
    4.     public static void Initialize() {
    5.         Reset();
    6.         itemList = JsonDataManager.LoadData();
    7.         ConstructItemDatabase();
    8.     }
    9.     public static void Reset() {
    10.         database.Clear();
    11.         itemList.Clear();
    12.     }
    13.     public static Item FetchItemById(int id) {
    14.  
    15.         for (int i = 0; i < database.Count; i++) {
    16.             if (database[i].Id == id) {
    17.                 return database[i];
    18.             }
    19.         }
    20.  
    21.         return null;
    22.     }
    23.  
    24.  
    25.     public static BaseItemTemplate FetchBaseItemTemplateById(int id) {
    26.         for (int i = 0; i < itemList.Count; i++) {
    27.             if (Int16.Parse(itemList[i].Id) == id) {
    28.                 return itemList[i];
    29.             }
    30.         }
    31.  
    32.         return null;
    33.     }
    34.  
    35.  
    36.     private static void ConstructItemDatabase() {
    37.         for (int i = 0; i < itemList.Count; i++)
    38.             {
    39.             Item newItem = new Item();
    40.             newItem.Id = Int16.Parse(itemList[i].Id);
    41.             newItem.Title = itemList[i].itemName;
    42.             newItem.Description = itemList[i].description;
    43.             newItem.Stackable = itemList[i].stackable;
    44.             newItem.Sprite = itemList[i].icon;
    45.  
    46.             database.Add(newItem);
    47.         }
    48.     }

    Thank you, ultimately I got it to work with the persistent data path and the suggestion of the other kind fellow:


    Code (CSharp):
    1.     private static string SaveFilePath => Application.persistentDataPath + "/data.json";
    2.  
    3.     private static string RecipeSaveFilePath => Application.persistentDataPath + "/recipes.json";
    That being said, each BaseItemTemplate (the list of scriptable objects I am producing from the json) no longer recognizes which other scriptable object type is inheriting it. In other words, I have a bunch of other scriptable objects that inherit BaseItemTemplate and I need to know what type they are for crafting etc. I would assume its related to the deserialization. How can I fix this?

    The updated code:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using Newtonsoft.Json;
    5. using Newtonsoft.Json.Linq;
    6. public static class JsonDataManager {
    7.     private static string SaveFilePath => Application.persistentDataPath + "/data.json";
    8.  
    9.     private static string RecipeSaveFilePath => Application.persistentDataPath + "/recipes.json";
    10.  
    11.  
    12.     public static void AddBaseItemTemplateToJson(BaseItemTemplate baseItemTemplate) {
    13.         // Load existing data from JSON
    14.         List<BaseItemTemplate> existingData = LoadData();
    15.  
    16.         // Add the new baseItemTemplate to the list
    17.         existingData.Add(baseItemTemplate);
    18.  
    19.         // Save the updated data to JSON
    20.         SaveData(existingData);
    21.     }
    22.  
    23.     public static void SaveData(List<BaseItemTemplate> data) {
    24.        
    25.         ItemTemplateWrapper wrapper = new ItemTemplateWrapper(data);
    26.         string jsonToSave = JsonConvert.SerializeObject(wrapper, Formatting.Indented,
    27. new JsonSerializerSettings {
    28.     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    29. });
    30.  
    31.         File.WriteAllText(SaveFilePath, jsonToSave);
    32.     }
    33.  
    34.     public static List<BaseItemTemplate> LoadData() {
    35.         string jsonToLoad = File.ReadAllText(SaveFilePath);
    36.  
    37.         ItemTemplateWrapper wrapper = JsonConvert.DeserializeObject<ItemTemplateWrapper>(jsonToLoad);
    38.  
    39.             //ItemTemplateWrapper wrapper = JsonUtility.FromJson<ItemTemplateWrapper>(jsonToLoad);
    40.             foreach(BaseItemTemplate item in wrapper.items) {
    41.             try {
    42.                 item.prefab = Resources.Load<GameObject>(item.prefabString);
    43.                 item.icon = Resources.Load<Sprite>(item.slug);
    44.             } catch {
    45.  
    46.             }
    47.            
    48.         }
    49.  
    50.         return wrapper.items;
    51.     }
    52.  
    53.     public static List<RecipeTemplate> LoadRecipeData() {
    54.  
    55.             string jsonToLoad = File.ReadAllText(RecipeSaveFilePath);
    56.        
    57.             RecipeTemplateWrapper wrapper = JsonUtility.FromJson<RecipeTemplateWrapper>(jsonToLoad);
    58.             return wrapper.recipes;
    59.     }
    60.  
    61.     public static void SaveRecipeData(List<RecipeTemplate> recipes) {
    62.         RecipeTemplateWrapper wrapper = new RecipeTemplateWrapper(recipes);
    63.         string jsonToSave = JsonUtility.ToJson(wrapper);
    64.         File.WriteAllText(RecipeSaveFilePath, jsonToSave);
    65.     }
    66. }
    67.  
    68. [System.Serializable]
    69. public class ItemTemplateWrapper {
    70.     public List<BaseItemTemplate> items;
    71.  
    72.     public ItemTemplateWrapper() {
    73.         items = new List<BaseItemTemplate>();
    74.     }
    75.  
    76.     public ItemTemplateWrapper(List<BaseItemTemplate> itemList) {
    77.         items = itemList;
    78.     }
    79. }
    80.  
    81. [System.Serializable]
    82. public class RecipeTemplateWrapper {
    83.     public List<RecipeTemplate> recipes;
    84.  
    85.     public RecipeTemplateWrapper() {
    86.         recipes = new List<RecipeTemplate>();
    87.     }
    88.  
    89.     public RecipeTemplateWrapper(List<RecipeTemplate> recipeList) {
    90.         recipes = recipeList;
    91.     }
    92. }
    93.  
    94.  
     
    h1ggs likes this.
  6. Hunter2e

    Hunter2e

    Joined:
    Feb 24, 2023
    Posts:
    3
    Thanks a lot! This worked in combination with using JSON .NET. I asked a follow up question on the other reply. I'd appreciate if you had any more helpful info regarding that :)