Skip to content

Instantly share code, notes, and snippets.

@jbltx
Last active March 28, 2021 01:01
Show Gist options
  • Save jbltx/be3adfa04fd074941d6cbec7381ea750 to your computer and use it in GitHub Desktop.
Save jbltx/be3adfa04fd074941d6cbec7381ea750 to your computer and use it in GitHub Desktop.
Unity-like Object Reference Resolution with JSON.NET
namespace UnityRuntime
{
class AssetDatabase
{
public static bool Contains(Object obj)
{
// normally we store ids of assets and check obj.id is inside the db.
// for simplicity let say any TaskList is an asset.
return obj is TaskList;
}
}
}
using System;
using System.Collections.Generic;
namespace UnityRuntime
{
class Task : Object
{
public DisplayInfo displayInfo { get; private set; } = new();
public bool isDone { get; set; }
}
class DisplayInfo
{
public string displayName { get; set; }
public string description { get; set; }
}
class TaskList : Object
{
public List<Task> tasks { get; } = new();
public DisplayInfo displayInfo { get; set; } = new();
public TaskList relatedTaskList { get; set; }
}
}
using Newtonsoft.Json;
namespace UnityRuntime
{
/// <summary>
/// Small utility class to encapsulate JSON.NET calls and settings
/// </summary>
public static class JsonSerializer
{
static Object s_RootObject;
static readonly JsonSerializerSettings k_Settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceResolverProvider = () => new Object.ReferenceResolver { rootObject = s_RootObject },
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
NullValueHandling = NullValueHandling.Include,
ContractResolver = new Object.ContractResolver()
};
/// <summary>
/// Serialize a <see cref="Object"/> instance to JSON format.
/// </summary>
/// <param name="obj">The <see cref="Object"/> to serialize</param>
/// <returns>A JSON <see cref="string"/> text.</returns>
public static string ToJson(Object obj)
{
s_RootObject = obj;
return JsonConvert.SerializeObject(obj, k_Settings);
}
/// <summary>
/// Deserialize a JSON text to a <see cref="Object"/> instance.
/// </summary>
/// <param name="json">The JSON text.</param>
/// <returns>The <see cref="Object"/> instance if the operation succeed, null otherwise.</returns>
public static Object FromJson(string json)
{
return JsonConvert.DeserializeObject<Object>(json, k_Settings);
}
/// <summary>
/// Deserialize a JSON text to a <see cref="Object"/> instance.
/// </summary>
/// <param name="json">The JSON text.</param>
/// <typeparam name="T">A derived class type from <see cref="Object"/> type.</typeparam>
/// <returns>The <see cref="Object"/> instance if the operation succeed, null otherwise.</returns>
public static T FromJson<T>(string json) where T : Object
{
return JsonConvert.DeserializeObject<T>(json, k_Settings);
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace UnityRuntime
{
[Serializable]
[JsonObject(IsReference = true)]
public abstract class Object
{
[JsonIgnore]
Guid m_Id = Guid.Empty;
bool m_IsDestroyed;
protected Object()
{
name = "New Object";
id = Guid.NewGuid();
}
static readonly Dictionary<Guid, Object> k_InMemoryMapping = new Dictionary<Guid, Object>();
[JsonProperty]
Guid id
{
get => m_Id;
set
{
if (value == Guid.Empty)
throw new Exception("ID can't be empty");
m_Id = value;
k_InMemoryMapping[m_Id] = this;
}
}
public Guid GetInstanceId()
{
return id;
}
[JsonProperty]
public string name { get; set; }
public static void Destroy(Object obj)
{
k_InMemoryMapping.Remove(obj.m_Id); // clear from db
obj.m_IsDestroyed = true;
}
public static Object Instantiate(Object original)
{
var obj = (Object)original.MemberwiseClone();
obj.name += " (Clone)";
obj.id = Guid.NewGuid();
return obj;
}
public override string ToString() => $"[{GetType().Name}] {name}";
public static implicit operator bool(Object obj) => obj is { m_IsDestroyed: false };
#nullable enable
internal class ObjectJsonConverter : JsonConverter<Object>
{
public override bool CanRead => true;
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, Object? value, Newtonsoft.Json.JsonSerializer serializer)
{
var obj = value!;
if (obj)
{
if (AssetDatabase.Contains(obj))
{
writer.WriteValue(obj.GetInstanceId().ToString());
}
else
{
var jsonObj = (JObject)JToken.FromObject(obj, serializer);
jsonObj.WriteTo(writer);
}
}
else
{
writer.WriteNull();
}
}
public override Object? ReadJson(JsonReader reader, Type objectType, Object? existingValue, bool hasExistingValue,
Newtonsoft.Json.JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.StartObject:
{
var jsonObj = JObject.Load(reader);
if (jsonObj.ContainsKey("$ref"))
return serializer.ReferenceResolver!.ResolveReference(serializer, (string)jsonObj["$ref"]!) as Object;
return jsonObj.ToObject(objectType, serializer) as Object;
}
case JsonToken.Null:
return null;
case JsonToken.String when reader.Value is string str:
{
var id = new Guid(str);
return k_InMemoryMapping.TryGetValue(id, out var storedObj) ? storedObj : null;
}
default:
throw new NotSupportedException("Unexpected token type");
}
}
}
#nullable disable
internal class ContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);
if (typeof(Object).IsAssignableFrom(prop.PropertyType))
{
prop.Converter = new ObjectJsonConverter();
}
else
{
var collectionType = prop.PropertyType?.GetInterface(nameof(ICollection));
if (collectionType != null)
{
var itemType = prop.PropertyType!.GetGenericArguments();
if (itemType.Length > 0 && typeof(Object).IsAssignableFrom(itemType[0]))
prop.ItemConverter = new ObjectJsonConverter();
if (typeof(Object).IsAssignableFrom(prop.PropertyType.GetElementType()))
prop.ItemConverter = new ObjectJsonConverter();
}
}
return prop;
}
}
internal class ReferenceResolver : IReferenceResolver
{
Dictionary<Guid, object> m_InFileMapping = new Dictionary<Guid, object>();
public Object rootObject { get; set; }
public object ResolveReference(object context, string reference)
{
var id = Guid.Parse(reference);
if (m_InFileMapping.ContainsKey(id))
return m_InFileMapping[id];
return k_InMemoryMapping.TryGetValue(id, out var obj) ? obj : null;
}
public string GetReference(object context, object value)
{
if (value is Object obj && AssetDatabase.Contains(obj))
{
if (!k_InMemoryMapping.ContainsValue(obj)) // should never happen
k_InMemoryMapping[obj.id] = obj;
return obj.id.ToString();
}
if (!m_InFileMapping.ContainsValue(value))
{
var id = (value is Object obj2) ? obj2.id : Guid.NewGuid();
m_InFileMapping[id] = value;
}
return m_InFileMapping
.First(kvp => kvp.Value == value)
.Key.ToString();
}
public bool IsReferenced(object context, object value)
{
if (value is Object obj && AssetDatabase.Contains(obj))
{
return value != rootObject;
}
return m_InFileMapping.ContainsValue(value);
}
public void AddReference(object context, string reference, object value)
{
var id = Guid.Parse(reference);
if (value is Object obj)
{
if (k_InMemoryMapping.ContainsKey(id))
{
#if DEBUG
Console.WriteLine($"Warning: Overwriting in-memory object {id}");
#else
throw new Exception($"Collision of IDs from the currently " +
$"deserialized object and an existing one in memory: ID {id}");
#endif
}
k_InMemoryMapping[id] = obj;
}
else
m_InFileMapping[id] = value;
}
}
}
}
using System;
namespace UnityRuntime
{
static class Program
{
static void Main(string[] args)
{
// A TaskList is an asset, it should be loaded using
// AssetDatabase.LoadObjectAtPath(); but
// for simplicity we just create it here with new();
var otherTaskList = new TaskList { name = "otherTaskList" };
var taskList = new TaskList
{
name = "taskListAsset",
displayInfo = { displayName = "My Tasks" },
relatedTaskList = otherTaskList,
};
var task1 = new Task
{
name = "task1",
displayInfo = { displayName = "Task 1" },
isDone = true,
};
var task2 = Object.Instantiate(task1) as Task;
if (!task2)
throw new NullReferenceException("task2 is null");
task2.displayInfo.displayName = "Task 2";
task2.isDone = false;
Console.WriteLine($"{task1}"); // [Task] task1
Console.WriteLine($"{task2}"); // [Task] task1 (Clone)
Console.WriteLine(task1 == task2); // False
Console.WriteLine(task1); // True
var task3 = Object.Instantiate(task2);
if (!task3)
throw new NullReferenceException("task3 is null");
Object.Destroy(task3);
if (task3)
throw new Exception("task3 is still alive after destruction");
taskList.tasks.Add(task1);
taskList.tasks.Add(task2);
taskList.tasks.Add(task2);
var json = JsonSerializer.ToJson(taskList);
Console.WriteLine(json);
// {
// "$id": "6fe2fb0d-fcd6-44a1-997b-4fb5ba006877",
// "tasks": [
// {
// "$id": "8da4f1f8-f6ae-4391-afdc-9a4eaf7f53e3",
// "displayInfo": {
// "displayName": "Task 2",
// "description": null
// },
// "isDone": true,
// "name": "task1",
// "id": "8da4f1f8-f6ae-4391-afdc-9a4eaf7f53e3"
// },
// {
// "$id": "caa887da-dfdc-48a0-a7ba-e9c883cac565",
// "displayInfo": {
// "displayName": "Task 2",
// "description": null
// },
// "isDone": false,
// "name": "task1 (Clone)",
// "id": "caa887da-dfdc-48a0-a7ba-e9c883cac565"
// },
// {
// "$ref": "caa887da-dfdc-48a0-a7ba-e9c883cac565"
// }
// ],
// "displayInfo": {
// "$id": "c42dfebd-6215-45ba-8491-0c09211521e1",
// "displayName": "My Tasks",
// "description": null
// },
// "relatedTaskList": {
// "$ref": "16d56f78-e8ff-4ed6-b2b9-e98359358d33"
// },
// "name": "taskListAsset",
// "id": "6fe2fb0d-fcd6-44a1-997b-4fb5ba006877"
// }
var newTaskList = JsonSerializer.FromJson<TaskList>(json);
Console.WriteLine(newTaskList.displayInfo.displayName); // My Tasks
Console.WriteLine(newTaskList.tasks[0] == task1); // False (not an asset, overwritten in memory)
Console.WriteLine(newTaskList.relatedTaskList == otherTaskList); // True (asset, found in memory)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment