Code/Util/SerializableObject.cs
using System.Text.Json;

namespace Nodebox.Util;

public class SerializableObject {
    public SerializableObject() { }
    public SerializableObject(object obj) : this() {
        SerializableType = obj.GetType();
        Value = obj;
    }

    public SerializableObject(Type type, object obj) : this() {
        SerializableType = type;
        UntypedValue = obj;
    }

    [JsonInclude]
    [Property]
    public SerializableType SerializableType { get; set; } = null;
    [JsonIgnore]
    [Hide]
    public Type Type => SerializableType?.Type;

    [JsonIgnore]
    private object _untypedValue = null;
    [JsonInclude]
    [Hide]
    protected object UntypedValue {
        get => _untypedValue;
        set {
            _untypedValue = value;
            _cachedValue = null;
            _cachedHashCode = null;
        }
    }

    [JsonIgnore]
    private object _cachedValue = null;
    [JsonIgnore]
    private int? _cachedHashCode = null;

    [Property]
    [JsonIgnore]
    public object Value {
        get {
            if (UntypedValue is not JsonElement jsonElement) {
                return UntypedValue;
            }

            if (_cachedHashCode != null && _cachedHashCode.Value == SerializableType.GetHashCode()) {
                return _cachedValue;
            }

            var options = new JsonSerializerOptions(JsonSerializerOptions.Default) {
                PropertyNameCaseInsensitive = true,
                NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
            };

            options.Converters.Add(new JsonStringEnumConverter(null, true));

            _cachedValue = jsonElement.Deserialize(Type, options);
            _cachedHashCode = SerializableType.GetHashCode();
            return _cachedValue;
        }
        set {
            if (UntypedValue != null) {
                Assert.AreEqual(UntypedValue.GetType(), Type);
            }

            UntypedValue = value;
            _cachedValue = null;
            _cachedHashCode = null;
            // _cachedValue = value;
            // _cachedHashCode = SerializableType.GetHashCode();
        }
    }

    public override string ToString() => SerializableType?.Type?.ToSimpleString() ?? "SerializableObject(null)";
}