applejag / Newtonsoft.Json-for-Unity.Converters

Converters of common Unity types for Newtonsoft.Json. Goes hand in hand with jilleJr/Newtonsoft.Json-for-Unity
https://github.com/jilleJr/Newtonsoft.Json-for-Unity.Converters
MIT License
322 stars 36 forks source link

Bug: Deserializing a Scriptable Object that contains a sprite reference throws error #69

Open HunterAhlquist opened 2 years ago

HunterAhlquist commented 2 years ago

Expected behavior

Serialized ScriptableObject with Sprite reference gets initialized through its converter.

Actual behavior

Throws a NullReferenceException:

Call Stack
NullReferenceException UnityEngine.Sprite.get_bounds () (at :0) (wrapper dynamic-method) System.Object.lambda_method(System.Runtime.CompilerServices.Closure,object) Newtonsoft.Json.Serialization.ExpressionValueProvider.GetValue (System.Object target) (at :0) Rethrow as JsonSerializationException: Error getting value from 'bounds' on 'UnityEngine.Sprite'. Newtonsoft.Json.Serialization.ExpressionValueProvider.GetValue (System.Object target) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CalculatePropertyDetails (Newtonsoft.Json.Serialization.JsonProperty property, Newtonsoft.Json.JsonConverter& propertyConverter, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerProperty, Newtonsoft.Json.JsonReader reader, System.Object target, System.Boolean& useExistingValue, System.Object& currentValue, Newtonsoft.Json.Serialization.JsonContract& propertyContract, System.Boolean& gottenCurrentValue, System.Boolean& ignoredValue) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue (Newtonsoft.Json.Serialization.JsonProperty property, Newtonsoft.Json.JsonConverter propertyConverter, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerProperty, Newtonsoft.Json.JsonReader reader, System.Object target) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject (System.Object newObject, Newtonsoft.Json.JsonReader reader, Newtonsoft.Json.Serialization.JsonObjectContract contract, Newtonsoft.Json.Serialization.JsonProperty member, System.String id) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue (Newtonsoft.Json.Serialization.JsonProperty property, Newtonsoft.Json.JsonConverter propertyConverter, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerProperty, Newtonsoft.Json.JsonReader reader, System.Object target) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject (System.Object newObject, Newtonsoft.Json.JsonReader reader, Newtonsoft.Json.Serialization.JsonObjectContract contract, Newtonsoft.Json.Serialization.JsonProperty member, System.String id) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) (at :0) Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType, System.Boolean checkAdditionalContent) (at :0) Newtonsoft.Json.JsonSerializer.DeserializeInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType) (at :0) Newtonsoft.Json.JsonSerializer.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType) (at :0) Newtonsoft.Json.JsonConvert.DeserializeObject (System.String value, System.Type type, Newtonsoft.Json.JsonSerializerSettings settings) (at :0) Newtonsoft.Json.JsonConvert.DeserializeObject[T] (System.String value, Newtonsoft.Json.JsonSerializerSettings settings) (at :0) Newtonsoft.Json.JsonConvert.DeserializeObject[T] (System.String value) (at :0) SOTester.Start () (at Assets/SOTester.cs:15)

Steps to reproduce

public class SOTester : MonoBehaviour {
    public TestSO obj;

    void Start() {
        string serialized = JsonConvert.SerializeObject(obj);
        obj = null;
        obj = JsonConvert.DeserializeObject<TestSO>(serialized);
    }
}
[CreateAssetMenu(fileName = "New SO", menuName = "Test Scriptable Object")]
public class TestSO : ScriptableObject {
    public string name;
    public Sprite icon;
}

Details

Host machine OS running Unity Editor 👉 Windows

Unity build target 👉 Windows, Mac, Linux

Newtonsoft.Json-for-Unity package version 👉 10.0.302

Newtonsoft.Json-for-Unity.Converters package version 👉 1.4.0

I was using Unity version 👉 2020.3.26f1

Checklist

applejag commented 2 years ago

Hello @HunterAhlquist, thanks for reporting this!

Sad to say that there's some technical issues with this, which is why this package don't support Sprites at the moment.

Some of the fields are no biggie, such as:

However, some other fields are less straight forward. Such as:

Especially the latter two makes it impossible to simply just create new objects on the fly when deserializing. Instead, I have to call Sprite.Create or something similar, which allocates a lot, does not reference your internal assets, and will duplicate the sprites if they are referenced multiple times in the project.

Depending on your use case, I suggest to either:

  1. Use AssetReference from the Addressables package, which is supported since v1.4.0 of Newtonsoft.Json-for-Unity.Converters.

    This would allow you to make sure to reuse the same exact asset without excessive allocations and texture duplications. The JSON would then only include the GUID of the asset.

  2. Use a custom type for Sprite, that includes all the fields you want to transfer via JSON. Ex:

    public class SpriteData
    {
       public Bounds bounds;
       public Rect rect;
       public Vector4 border;
       public float pixelsPerUnit;
       public float spriteAtlasTextureScale;
       public Vector2 pivot;
       public bool packed;
       // add other fields you care about
    }

    Use this if your use case orients around the metadata for a sprite. What you'd do is to new SpriteData() and then assign all fields based on a Unity Sprite asset. If you want to apply the state back to a Unity Sprite asset then you do the reverse and assign the fields from the SpriteData on to the Unity Sprite assets.

    Then to get your ScriptableObject to not error because of the Sprite reference, you can add the [JsonIgnore] attribute to that field.

  3. Write a custom JSON converter yourself. If you don't care about the asset reference loss, sprite duplications overhead, or other things like that, then you can create a SpriteConverter that suits your use case precisely.