Last active
March 28, 2021 01:01
-
-
Save jbltx/be3adfa04fd074941d6cbec7381ea750 to your computer and use it in GitHub Desktop.
Unity-like Object Reference Resolution with JSON.NET
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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