// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace FlaxEngine.Json.JsonCustomSerializers { internal class ExtendedDefaultContractResolver : DefaultContractResolver { private readonly Type _flaxType = typeof(Object); private static readonly JsonConverter InterfaceObjectReferenceConverterInstance = new InterfaceObjectReferenceConverter(); private readonly Type[] AttributesIgnoreList = { typeof(NonSerializedAttribute), typeof(NoSerializeAttribute) }; private readonly Type[] AttributesIgnoreListManaged = { typeof(UnmanagedAttribute), typeof(NonSerializedAttribute), typeof(NoSerializeAttribute) }; private readonly Type[] _attributesIgnoreList; public ExtendedDefaultContractResolver(bool isManagedOnly) { _attributesIgnoreList = isManagedOnly ? AttributesIgnoreListManaged : AttributesIgnoreList; } private static bool HasObjectInterfaceReferenceAttribute(IEnumerable attributes) { return attributes.Any(x => x is ScriptingObjectInterfaceReferenceAttribute || x is SoftObjectInterfaceReferenceAttribute); } private static Type GetCollectionItemType(Type type) { if (type.IsArray) return type.GetElementType(); if (!type.IsGenericType || type == typeof(string)) return null; var types = type.GetInterfaces().Concat(new[] { type }); var dictionaryType = types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)); if (dictionaryType != null) return dictionaryType.GetGenericArguments()[1]; var enumerableType = types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)); return enumerableType?.GetGenericArguments()[0]; } private static void SetupInterfaceObjectReferenceItems(JsonContainerContract contract, Type itemType) { if (itemType?.IsInterface == true) { contract.ItemReferenceLoopHandling = ReferenceLoopHandling.Serialize; contract.ItemConverter = InterfaceObjectReferenceConverterInstance; } } private void SetupObjectReferenceProperty(JsonProperty jsonProperty, Type type, IEnumerable attributes) { var hasObjectInterfaceReferenceAttribute = HasObjectInterfaceReferenceAttribute(attributes); if (_flaxType.IsAssignableFrom(type) || (type.IsInterface && hasObjectInterfaceReferenceAttribute)) { jsonProperty.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; jsonProperty.Converter = JsonSerializer.ObjectConverter; } if (hasObjectInterfaceReferenceAttribute && GetCollectionItemType(type)?.IsInterface == true) { jsonProperty.ItemReferenceLoopHandling = ReferenceLoopHandling.Serialize; jsonProperty.ItemConverter = JsonSerializer.ObjectConverter; } } private sealed class InterfaceObjectReferenceConverter : JsonConverter { public override unsafe void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) { if (value is Object obj) { var id = obj.ID; writer.WriteValue(JsonSerializer.GetStringID(&id)); } else if (value == null) { writer.WriteNull(); } else { serializer.Serialize(writer, value, value.GetType()); } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) { if (reader.TokenType == JsonToken.String && JsonSerializer.TryParseID((string)reader.Value, out var id)) { return Object.Find(ref id, objectType, true); } if (reader.TokenType == JsonToken.Null) return null; // objectType is the same interface item type that selected this converter. Passing it back to // Newtonsoft can cause this converter to be chosen again and recurse until the stack overflows. return Newtonsoft.Json.Linq.JToken.Load(reader).ToObject(serializer); } public override bool CanConvert(Type objectType) { return objectType.IsInterface; } } /// protected override JsonContract CreateContract(Type objectType) { var contract = base.CreateContract(objectType); // Override contract for Flax objects if (_flaxType.IsAssignableFrom(objectType)) { ((JsonObjectContract)contract).ItemReferenceLoopHandling = ReferenceLoopHandling.Serialize; } return contract; } /// protected override JsonArrayContract CreateArrayContract(Type objectType) { var contract = base.CreateArrayContract(objectType); SetupInterfaceObjectReferenceItems(contract, contract.CollectionItemType); return contract; } /// protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { var contract = base.CreateDictionaryContract(objectType); SetupInterfaceObjectReferenceItems(contract, contract.DictionaryValueType); // Override contract to save enums keys as integer if (contract.DictionaryKeyType?.IsEnum ?? false) { var enumType = contract.DictionaryKeyType; contract.DictionaryKeyResolver = name => { try { var e = Enum.Parse(enumType, name); name = Convert.ToInt32(e).ToString(); } catch { // Ignore errors } return name; }; } return contract; } protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var result = new List(fields.Length + properties.Length); for (int i = 0; i < fields.Length; i++) { var f = fields[i]; var attributes = f.GetCustomAttributes(); // Serialize non-public fields only with a proper attribute if (!f.IsPublic && !attributes.Any(x => x is SerializeAttribute)) continue; // Check if has attribute to skip serialization bool noSerialize = false; foreach (var attribute in attributes) { if (_attributesIgnoreList.Contains(attribute.GetType())) { noSerialize = true; break; } } if (noSerialize) continue; var jsonProperty = CreateProperty(f, memberSerialization); jsonProperty.Writable = true; jsonProperty.Readable = true; SetupObjectReferenceProperty(jsonProperty, f.FieldType, attributes); result.Add(jsonProperty); } for (int i = 0; i < properties.Length; i++) { var p = properties[i]; // Serialize only properties with read/write if (!(p.CanRead && p.CanWrite && p.GetIndexParameters().GetLength(0) == 0)) continue; var attributes = p.GetCustomAttributes(); // Serialize non-public properties only with a proper attribute if ((!p.GetMethod.IsPublic || !p.SetMethod.IsPublic) && !attributes.Any(x => x is SerializeAttribute)) continue; // Check if has attribute to skip serialization bool noSerialize = false; foreach (var attribute in attributes) { if (_attributesIgnoreList.Contains(attribute.GetType())) { noSerialize = true; break; } } if (noSerialize) continue; var isObsolete = attributes.Any(x => x is ObsoleteAttribute); var jsonProperty = CreateProperty(p, memberSerialization); jsonProperty.Writable = true; jsonProperty.Readable = !isObsolete; SetupObjectReferenceProperty(jsonProperty, p.PropertyType, attributes); result.Add(jsonProperty); } return result; } } }