When using JsonUtility.ToJson() on instances of structs that implement ISerializationCallbackReceiver the effects of OnBeforeSerialize() are not visible on the resulting string.
To be more specific, with the following code:
using System;
using UnityEngine;
public class StructCustomSerializationTest : MonoBehaviour
{
[ContextMenu("Test")]
public void Test()
{
StructWithCustomSerialization withCustomSerialization = new() { x = 1, y = 2};
Debug.Log($"withCustomSerialization = {JsonUtility.ToJson(withCustomSerialization)}");
}
}
[Serializable]
public struct StructWithCustomSerialization : ISerializationCallbackReceiver
{
public float x;
public float y;
public float sum;
public void OnAfterDeserialize()
{
}
public void OnBeforeSerialize()
{
sum = x + y;
}
}
Note that just changing from struct to class works as expected. I think this is a bug due to OnBeforeSerialize() being called on some copy of the struct but the serialization happening on the original struct. I tried reading the documentation on custom serialization (Unity - Manual: Custom serialization) but it does not mention nothing relevant regarding this.
It used to mention it that it doesn’t work on structs. It may have been an oversight that this was removed. Though on the other hand it does work “somewhat” so maybe that’s the reason they removed the information, but we don’t really know for sure. Yes, if Unity is actually using the interface as an interface, the instance would be boxed and as a result changes would not affect the actual struct. Maybe there are cases where it actually works, but it’s something I generally would not expect.
Interfaces are always reference types. While it’s possible to use some hacky compiler and reflection magic to work with structs that implement interfaces, when used in the classical sense you would always get a boxed version.
It works fine with structs, the specific issue you’re encountering is that it doesn’t work when you pass a struct to JsonUtility. If the struct is part of a serializable plain object or Unity object, then sum does end up with the correct value in the JSON.
JSON from MonoBehaviour: {“test”:{“one”:1,“two”:2,“sum”:3}}
JSON from ScriptableObject: {“test”:{“one”:1,“two”:2,“sum”:3}}
JSON from object: {“test”:{“one”:1,“two”:2,“sum”:3}}
JSON from struct: {“one”:1,“two”:2,“sum”:0}
Code
using System;
using UnityEditor;
using UnityEngine;
[Serializable]
public struct SerializationTest : ISerializationCallbackReceiver
{
public int one;
public int two;
public int sum;
public void OnBeforeSerialize()
{
sum = one + two;
Debug.Log($"SerializationTest.OnBeforeSerialize sum = {one} + {two} = {sum}");
}
public void OnAfterDeserialize()
{
sum = 0;
Debug.Log($"SerializationTest.OnAfterDeserialize sum = 0");
}
}
[Serializable]
public class SerializationCallbackTestObject
{
public SerializationTest test;
}
public class SerializationCallbackTestScript : MonoBehaviour
{
public SerializationTest test;
}
public class SerializationCallbackTestSO : ScriptableObject
{
public SerializationTest test;
}
public static class Tests
{
[MenuItem("Commands/Serialization Test/Struct Test")]
static void Test1()
{
var test = new SerializationTest() { one = 1, two = 2 };
Debug.Log($"JSON from struct: {JsonUtility.ToJson(test)}");
}
[MenuItem("Commands/Serialization Test/MonoBehaviour Test")]
static void Test2()
{
var go = new GameObject("Test");
var script = go.AddComponent<SerializationCallbackTestScript>();
script.test = new SerializationTest() { one = 1, two = 2 };
Debug.Log($"JSON from MonoBehaviour: {JsonUtility.ToJson(script)}");
UnityEngine.Object.DestroyImmediate(go);
}
[MenuItem("Commands/Serialization Test/ScriptableObject Test")]
static void Test3()
{
var so = ScriptableObject.CreateInstance<SerializationCallbackTestSO>();
so.test = new SerializationTest() { one = 1, two = 2 };
Debug.Log($"JSON from ScriptableObject: {JsonUtility.ToJson(so)}");
UnityEngine.Object.DestroyImmediate(so);
}
[MenuItem("Commands/Serialization Test/Plain Object Test")]
static void Test4()
{
var obj = new SerializationCallbackTestObject();
obj.test = new SerializationTest() { one = 1, two = 2 };
Debug.Log($"JSON from object: {JsonUtility.ToJson(obj)}");
}
}
Interestingly, the OnBeforeSerialize in the last call has corrupted data, suggesting there’s more going wrong than just a struct copy (on Unity 6.0.29).
SerializationTest.OnBeforeSerialize sum = -196244104 + 3 = -196244101
I suspect this could be worth submitting a bug report for.
Just double checked again and yes, it seems like any mention to structs not being supported has been removed, which seems to me a bit weird after seeing above example.