applejag/Newtonsoft.Json-for-Unity.Converters

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

HunterAhlquist opened this issue ยท 1 comments

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

  • New project
  • Import jillejr.newtonsoft.json-for-unity via UPM
  • Import jillejr.newtonsoft.json-for-unity.converters via UPM
  • Add following script to an object in a scene:
public class SOTester : MonoBehaviour {
    public TestSO obj;

    void Start() {
        string serialized = JsonConvert.SerializeObject(obj);
        obj = null;
        obj = JsonConvert.DeserializeObject<TestSO>(serialized);
    }
}
  • Create the following ScriptableObject type:
[CreateAssetMenu(fileName = "New SO", menuName = "Test Scriptable Object")]
public class TestSO : ScriptableObject {
    public string name;
    public Sprite icon;
}
  • Create an instance of TestSO and populate with valid data
  • Play the scene.
  • Object will serialize, but will not deserialize due to the Sprite reference in the type.

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

  • Shutdown Unity, deleted the /Library folder, opened project again in Unity, and problem still remains.
  • Checked to be using latest version of the package.

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:

  • Sprite.bounds
  • Sprite.rect
  • Sprite.border
  • Sprite.pixelsPerUnit
  • Sprite.spriteAtlasTextureScale
  • Sprite.pivot
  • Sprite.packed
  • etc

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

  • Sprite.texture
  • Sprite.associatedAlphaSplitTexture
  • Sprite.m_CachedPtr (inherited from UnityEngine.Object)
  • Sprite.m_InstanceID (inherited from UnityEngine.Object)

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.